├── .github ├── CODEOWNERS ├── dependabot.yaml └── workflows │ └── test.yml ├── .gitignore ├── .go-version ├── CHANGELOG.md ├── LICENSE ├── README.md ├── args.go ├── call.go ├── convert.go ├── convert_test.go ├── doc.go ├── error.go ├── filter.go ├── func.go ├── func_test.go ├── go.mod ├── go.sum ├── graph.go ├── internal └── graph │ ├── dfs.go │ ├── dijkstra.go │ ├── dijkstra_test.go │ ├── graph.go │ ├── kahn.go │ ├── path.go │ ├── path_test.go │ ├── tarjan.go │ └── vertex.go ├── redefine.go ├── redefine_test.go ├── result.go ├── struct.go ├── struct_test.go ├── value_set.go └── valuekind_string.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Each line is a file pattern followed by one or more owners. 2 | # More on CODEOWNERS files: https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners 3 | 4 | # Default owner 5 | * @hashicorp/team-ip-compliance 6 | 7 | # Add override rules below. Each line is a file/folder pattern followed by one or more owners. 8 | # Being an owner means those groups or individuals will be added as reviewers to PRs affecting 9 | # those areas of the code. 10 | # Examples: 11 | # /docs/ @docs-team 12 | # *.js @js-team 13 | # *.go @go-team -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | version: 2 5 | 6 | updates: 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | commit-message: 12 | prefix: "[chore] : " 13 | 14 | - package-ecosystem: "gomod" 15 | directory: "/" 16 | schedule: 17 | interval: "weekly" 18 | commit-message: 19 | prefix: "[chore] : " -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.23] 8 | os: [ubuntu-latest] 9 | runs-on: ${{ matrix.os }} 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | - name: Checkout code 16 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 17 | - name: Run golangci-lint 18 | uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 19 | with: 20 | args: --timeout=5m 21 | - name: Test and generate coverage report 22 | run: go test -v -coverprofile=coverage.out ./... 23 | - name: Upload coverage report 24 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 25 | with: 26 | path: coverage.out 27 | name: coverage-report 28 | - name: Display coverage report 29 | run: go tool cover -func=coverage.out 30 | - name: Build 31 | run: go build ./... 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /.go-version: -------------------------------------------------------------------------------- 1 | 1.23 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Unreleased 2 | 3 | ### Improvements 4 | 5 | ### Changes 6 | 7 | ### Fixed 8 | 9 | ### Security -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 HashiCorp, Inc. 2 | 3 | Mozilla Public License Version 2.0 4 | ================================== 5 | 6 | 1. Definitions 7 | -------------- 8 | 9 | 1.1. "Contributor" 10 | means each individual or legal entity that creates, contributes to 11 | the creation of, or owns Covered Software. 12 | 13 | 1.2. "Contributor Version" 14 | means the combination of the Contributions of others (if any) used 15 | by a Contributor and that particular Contributor's Contribution. 16 | 17 | 1.3. "Contribution" 18 | means Covered Software of a particular Contributor. 19 | 20 | 1.4. "Covered Software" 21 | means Source Code Form to which the initial Contributor has attached 22 | the notice in Exhibit A, the Executable Form of such Source Code 23 | Form, and Modifications of such Source Code Form, in each case 24 | including portions thereof. 25 | 26 | 1.5. "Incompatible With Secondary Licenses" 27 | means 28 | 29 | (a) that the initial Contributor has attached the notice described 30 | in Exhibit B to the Covered Software; or 31 | 32 | (b) that the Covered Software was made available under the terms of 33 | version 1.1 or earlier of the License, but not also under the 34 | terms of a Secondary License. 35 | 36 | 1.6. "Executable Form" 37 | means any form of the work other than Source Code Form. 38 | 39 | 1.7. "Larger Work" 40 | means a work that combines Covered Software with other material, in 41 | a separate file or files, that is not Covered Software. 42 | 43 | 1.8. "License" 44 | means this document. 45 | 46 | 1.9. "Licensable" 47 | means having the right to grant, to the maximum extent possible, 48 | whether at the time of the initial grant or subsequently, any and 49 | all of the rights conveyed by this License. 50 | 51 | 1.10. "Modifications" 52 | means any of the following: 53 | 54 | (a) any file in Source Code Form that results from an addition to, 55 | deletion from, or modification of the contents of Covered 56 | Software; or 57 | 58 | (b) any new file in Source Code Form that contains any Covered 59 | Software. 60 | 61 | 1.11. "Patent Claims" of a Contributor 62 | means any patent claim(s), including without limitation, method, 63 | process, and apparatus claims, in any patent Licensable by such 64 | Contributor that would be infringed, but for the grant of the 65 | License, by the making, using, selling, offering for sale, having 66 | made, import, or transfer of either its Contributions or its 67 | Contributor Version. 68 | 69 | 1.12. "Secondary License" 70 | means either the GNU General Public License, Version 2.0, the GNU 71 | Lesser General Public License, Version 2.1, the GNU Affero General 72 | Public License, Version 3.0, or any later versions of those 73 | licenses. 74 | 75 | 1.13. "Source Code Form" 76 | means the form of the work preferred for making modifications. 77 | 78 | 1.14. "You" (or "Your") 79 | means an individual or a legal entity exercising rights under this 80 | License. For legal entities, "You" includes any entity that 81 | controls, is controlled by, or is under common control with You. For 82 | purposes of this definition, "control" means (a) the power, direct 83 | or indirect, to cause the direction or management of such entity, 84 | whether by contract or otherwise, or (b) ownership of more than 85 | fifty percent (50%) of the outstanding shares or beneficial 86 | ownership of such entity. 87 | 88 | 2. License Grants and Conditions 89 | -------------------------------- 90 | 91 | 2.1. Grants 92 | 93 | Each Contributor hereby grants You a world-wide, royalty-free, 94 | non-exclusive license: 95 | 96 | (a) under intellectual property rights (other than patent or trademark) 97 | Licensable by such Contributor to use, reproduce, make available, 98 | modify, display, perform, distribute, and otherwise exploit its 99 | Contributions, either on an unmodified basis, with Modifications, or 100 | as part of a Larger Work; and 101 | 102 | (b) under Patent Claims of such Contributor to make, use, sell, offer 103 | for sale, have made, import, and otherwise transfer either its 104 | Contributions or its Contributor Version. 105 | 106 | 2.2. Effective Date 107 | 108 | The licenses granted in Section 2.1 with respect to any Contribution 109 | become effective for each Contribution on the date the Contributor first 110 | distributes such Contribution. 111 | 112 | 2.3. Limitations on Grant Scope 113 | 114 | The licenses granted in this Section 2 are the only rights granted under 115 | this License. No additional rights or licenses will be implied from the 116 | distribution or licensing of Covered Software under this License. 117 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 118 | Contributor: 119 | 120 | (a) for any code that a Contributor has removed from Covered Software; 121 | or 122 | 123 | (b) for infringements caused by: (i) Your and any other third party's 124 | modifications of Covered Software, or (ii) the combination of its 125 | Contributions with other software (except as part of its Contributor 126 | Version); or 127 | 128 | (c) under Patent Claims infringed by Covered Software in the absence of 129 | its Contributions. 130 | 131 | This License does not grant any rights in the trademarks, service marks, 132 | or logos of any Contributor (except as may be necessary to comply with 133 | the notice requirements in Section 3.4). 134 | 135 | 2.4. Subsequent Licenses 136 | 137 | No Contributor makes additional grants as a result of Your choice to 138 | distribute the Covered Software under a subsequent version of this 139 | License (see Section 10.2) or under the terms of a Secondary License (if 140 | permitted under the terms of Section 3.3). 141 | 142 | 2.5. Representation 143 | 144 | Each Contributor represents that the Contributor believes its 145 | Contributions are its original creation(s) or it has sufficient rights 146 | to grant the rights to its Contributions conveyed by this License. 147 | 148 | 2.6. Fair Use 149 | 150 | This License is not intended to limit any rights You have under 151 | applicable copyright doctrines of fair use, fair dealing, or other 152 | equivalents. 153 | 154 | 2.7. Conditions 155 | 156 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 157 | in Section 2.1. 158 | 159 | 3. Responsibilities 160 | ------------------- 161 | 162 | 3.1. Distribution of Source Form 163 | 164 | All distribution of Covered Software in Source Code Form, including any 165 | Modifications that You create or to which You contribute, must be under 166 | the terms of this License. You must inform recipients that the Source 167 | Code Form of the Covered Software is governed by the terms of this 168 | License, and how they can obtain a copy of this License. You may not 169 | attempt to alter or restrict the recipients' rights in the Source Code 170 | Form. 171 | 172 | 3.2. Distribution of Executable Form 173 | 174 | If You distribute Covered Software in Executable Form then: 175 | 176 | (a) such Covered Software must also be made available in Source Code 177 | Form, as described in Section 3.1, and You must inform recipients of 178 | the Executable Form how they can obtain a copy of such Source Code 179 | Form by reasonable means in a timely manner, at a charge no more 180 | than the cost of distribution to the recipient; and 181 | 182 | (b) You may distribute such Executable Form under the terms of this 183 | License, or sublicense it under different terms, provided that the 184 | license for the Executable Form does not attempt to limit or alter 185 | the recipients' rights in the Source Code Form under this License. 186 | 187 | 3.3. Distribution of a Larger Work 188 | 189 | You may create and distribute a Larger Work under terms of Your choice, 190 | provided that You also comply with the requirements of this License for 191 | the Covered Software. If the Larger Work is a combination of Covered 192 | Software with a work governed by one or more Secondary Licenses, and the 193 | Covered Software is not Incompatible With Secondary Licenses, this 194 | License permits You to additionally distribute such Covered Software 195 | under the terms of such Secondary License(s), so that the recipient of 196 | the Larger Work may, at their option, further distribute the Covered 197 | Software under the terms of either this License or such Secondary 198 | License(s). 199 | 200 | 3.4. Notices 201 | 202 | You may not remove or alter the substance of any license notices 203 | (including copyright notices, patent notices, disclaimers of warranty, 204 | or limitations of liability) contained within the Source Code Form of 205 | the Covered Software, except that You may alter any license notices to 206 | the extent required to remedy known factual inaccuracies. 207 | 208 | 3.5. Application of Additional Terms 209 | 210 | You may choose to offer, and to charge a fee for, warranty, support, 211 | indemnity or liability obligations to one or more recipients of Covered 212 | Software. However, You may do so only on Your own behalf, and not on 213 | behalf of any Contributor. You must make it absolutely clear that any 214 | such warranty, support, indemnity, or liability obligation is offered by 215 | You alone, and You hereby agree to indemnify every Contributor for any 216 | liability incurred by such Contributor as a result of warranty, support, 217 | indemnity or liability terms You offer. You may include additional 218 | disclaimers of warranty and limitations of liability specific to any 219 | jurisdiction. 220 | 221 | 4. Inability to Comply Due to Statute or Regulation 222 | --------------------------------------------------- 223 | 224 | If it is impossible for You to comply with any of the terms of this 225 | License with respect to some or all of the Covered Software due to 226 | statute, judicial order, or regulation then You must: (a) comply with 227 | the terms of this License to the maximum extent possible; and (b) 228 | describe the limitations and the code they affect. Such description must 229 | be placed in a text file included with all distributions of the Covered 230 | Software under this License. Except to the extent prohibited by statute 231 | or regulation, such description must be sufficiently detailed for a 232 | recipient of ordinary skill to be able to understand it. 233 | 234 | 5. Termination 235 | -------------- 236 | 237 | 5.1. The rights granted under this License will terminate automatically 238 | if You fail to comply with any of its terms. However, if You become 239 | compliant, then the rights granted under this License from a particular 240 | Contributor are reinstated (a) provisionally, unless and until such 241 | Contributor explicitly and finally terminates Your grants, and (b) on an 242 | ongoing basis, if such Contributor fails to notify You of the 243 | non-compliance by some reasonable means prior to 60 days after You have 244 | come back into compliance. Moreover, Your grants from a particular 245 | Contributor are reinstated on an ongoing basis if such Contributor 246 | notifies You of the non-compliance by some reasonable means, this is the 247 | first time You have received notice of non-compliance with this License 248 | from such Contributor, and You become compliant prior to 30 days after 249 | Your receipt of the notice. 250 | 251 | 5.2. If You initiate litigation against any entity by asserting a patent 252 | infringement claim (excluding declaratory judgment actions, 253 | counter-claims, and cross-claims) alleging that a Contributor Version 254 | directly or indirectly infringes any patent, then the rights granted to 255 | You by any and all Contributors for the Covered Software under Section 256 | 2.1 of this License shall terminate. 257 | 258 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 259 | end user license agreements (excluding distributors and resellers) which 260 | have been validly granted by You or Your distributors under this License 261 | prior to termination shall survive termination. 262 | 263 | ************************************************************************ 264 | * * 265 | * 6. Disclaimer of Warranty * 266 | * ------------------------- * 267 | * * 268 | * Covered Software is provided under this License on an "as is" * 269 | * basis, without warranty of any kind, either expressed, implied, or * 270 | * statutory, including, without limitation, warranties that the * 271 | * Covered Software is free of defects, merchantable, fit for a * 272 | * particular purpose or non-infringing. The entire risk as to the * 273 | * quality and performance of the Covered Software is with You. * 274 | * Should any Covered Software prove defective in any respect, You * 275 | * (not any Contributor) assume the cost of any necessary servicing, * 276 | * repair, or correction. This disclaimer of warranty constitutes an * 277 | * essential part of this License. No use of any Covered Software is * 278 | * authorized under this License except under this disclaimer. * 279 | * * 280 | ************************************************************************ 281 | 282 | ************************************************************************ 283 | * * 284 | * 7. Limitation of Liability * 285 | * -------------------------- * 286 | * * 287 | * Under no circumstances and under no legal theory, whether tort * 288 | * (including negligence), contract, or otherwise, shall any * 289 | * Contributor, or anyone who distributes Covered Software as * 290 | * permitted above, be liable to You for any direct, indirect, * 291 | * special, incidental, or consequential damages of any character * 292 | * including, without limitation, damages for lost profits, loss of * 293 | * goodwill, work stoppage, computer failure or malfunction, or any * 294 | * and all other commercial damages or losses, even if such party * 295 | * shall have been informed of the possibility of such damages. This * 296 | * limitation of liability shall not apply to liability for death or * 297 | * personal injury resulting from such party's negligence to the * 298 | * extent applicable law prohibits such limitation. Some * 299 | * jurisdictions do not allow the exclusion or limitation of * 300 | * incidental or consequential damages, so this exclusion and * 301 | * limitation may not apply to You. * 302 | * * 303 | ************************************************************************ 304 | 305 | 8. Litigation 306 | ------------- 307 | 308 | Any litigation relating to this License may be brought only in the 309 | courts of a jurisdiction where the defendant maintains its principal 310 | place of business and such litigation shall be governed by laws of that 311 | jurisdiction, without reference to its conflict-of-law provisions. 312 | Nothing in this Section shall prevent a party's ability to bring 313 | cross-claims or counter-claims. 314 | 315 | 9. Miscellaneous 316 | ---------------- 317 | 318 | This License represents the complete agreement concerning the subject 319 | matter hereof. If any provision of this License is held to be 320 | unenforceable, such provision shall be reformed only to the extent 321 | necessary to make it enforceable. Any law or regulation which provides 322 | that the language of a contract shall be construed against the drafter 323 | shall not be used to construe this License against a Contributor. 324 | 325 | 10. Versions of the License 326 | --------------------------- 327 | 328 | 10.1. New Versions 329 | 330 | Mozilla Foundation is the license steward. Except as provided in Section 331 | 10.3, no one other than the license steward has the right to modify or 332 | publish new versions of this License. Each version will be given a 333 | distinguishing version number. 334 | 335 | 10.2. Effect of New Versions 336 | 337 | You may distribute the Covered Software under the terms of the version 338 | of the License under which You originally received the Covered Software, 339 | or under the terms of any subsequent version published by the license 340 | steward. 341 | 342 | 10.3. Modified Versions 343 | 344 | If you create software not governed by this License, and you want to 345 | create a new license for such software, you may create and use a 346 | modified version of this License if you rename the license and remove 347 | any references to the name of the license steward (except to note that 348 | such modified license differs from this License). 349 | 350 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 351 | Licenses 352 | 353 | If You choose to distribute Source Code Form that is Incompatible With 354 | Secondary Licenses under the terms of this version of the License, the 355 | notice described in Exhibit B of this License must be attached. 356 | 357 | Exhibit A - Source Code Form License Notice 358 | ------------------------------------------- 359 | 360 | This Source Code Form is subject to the terms of the Mozilla Public 361 | License, v. 2.0. If a copy of the MPL was not distributed with this 362 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 363 | 364 | If it is not possible or desirable to put the notice in a particular 365 | file, then You may include the notice in a location (such as a LICENSE 366 | file in a relevant directory) where a recipient would be likely to look 367 | for such a notice. 368 | 369 | You may add additional accurate notices of copyright ownership. 370 | 371 | Exhibit B - "Incompatible With Secondary Licenses" Notice 372 | --------------------------------------------------------- 373 | 374 | This Source Code Form is "Incompatible With Secondary Licenses", as 375 | defined by the Mozilla Public License, v. 2.0. 376 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-argmapper [![Godoc](https://godoc.org/github.com/hashicorp/go-argmapper?status.svg)](https://godoc.org/github.com/hashicorp/go-argmapper) 2 | 3 | go-argmapper is a dependency-injection library for Go that supports 4 | automatically chaining conversion functions to reach desired results. 5 | go-argmapper is designed for runtime, reflection-based dependency injection. 6 | 7 | **API Status: Mostly Stable.** We have released HashiCorp products using 8 | this library successfully, so we don't think the API will change significantly. 9 | For the time being, we're retaining the 0.x version numbers to note that we 10 | may still change the API and to recognize that the library has only been 11 | used in the real world for a short period of time. 12 | 13 | ## Features 14 | 15 | **Named parameter matching.** go-argmapper can match on named arguments, 16 | so you can say that `from int` is different from `to int` when calling 17 | the same function. 18 | 19 | **Typed parameter matching.** go-argmapper can match on types, including 20 | interfaces and interface implementations. This enables the common 21 | dependency-injection pattern of fulfilling an interface. 22 | 23 | **"Subtype" labels for overloaded types.** Values can be labeled with a "subtype" key (a string) 24 | for more fine-grained matching. A real-world use case of this is 25 | [protobuf Any values](https://developers.google.com/protocol-buffers/docs/proto3#any). 26 | The subtype of these values can be the protobuf message name. This enables 27 | separating name, type, and subtype for more fine-grained matching. 28 | 29 | **Automatic conversion function chaining.** You can configure multiple 30 | "conversion functions" that can take some set of values and return another 31 | set of values and go-argmapper will automatically call them in the correct 32 | order if necessary to reach your desired function parameter types. 33 | 34 | **Function redefinition in terms of certain types.** Functions can be 35 | "redefined" to take as input and/or output values that match user-provided 36 | filters. go-argmapper will automatically call proper conversion functions 37 | to reach the target function. 38 | 39 | **Type conversion API.** In addition to function calling, you can use the 40 | automatic conversion function chaining to convert some input values to 41 | any target value. go-argmapper will tell you (via an error) if this is not 42 | possible. 43 | 44 | ## Examples 45 | 46 | ### Basic Dependency Injection 47 | 48 | The example below shows common, basic dependency injection. 49 | 50 | ```go 51 | // This is our target function. It wants some Writer implementation. 52 | target, err := argmapper.NewFunc(func(w io.Writer) { 53 | // ... use the writer ... 54 | }) 55 | 56 | // This is a provider that provides our io.Writer. You can imagine that 57 | // this may differ between test/prod, configs, etc. 58 | provider := func() io.Writer { return bytes.NewBuffer(nil) } 59 | 60 | // Call our function. This will call our provider to create an io.Writer 61 | // and then call our target function. 62 | result := target.Call(argmapper.Converter(provider)) 63 | if result.Err() != nil { 64 | panic(result.Err()) 65 | } 66 | ``` 67 | 68 | The key thing happening here is that we're registering the `provider` 69 | function as a "converter." argmapper will automatically find some converter 70 | to provide any values we're looking for. 71 | 72 | ### Named and Typed Values 73 | 74 | The example below shows both named and typed parameters in use. 75 | 76 | ```go 77 | target, err := argmapper.NewFunc(func(input struct { 78 | // This tells argmapper to fill the values in this struct rather 79 | // than provide a value for the entire struct. 80 | argmapper.Struct 81 | 82 | A int 83 | B int 84 | Prefix string 85 | }) string { 86 | return fmt.Sprintf("%s: %d", in.Prefix, in.A*in.B) 87 | }) 88 | 89 | result := target.Call( 90 | argmapper.Named("a", 21), 91 | argmapper.Named("b", 2), 92 | argmapper.Typed("our value is"), 93 | ) 94 | if result.Err() != nil { 95 | panic(result.Err()) 96 | } 97 | 98 | // This prints: "our value is: 42" 99 | println(result.Out(0).(string)) 100 | ``` 101 | 102 | Both `A` and `B` are of the same type, but are matched on their names. 103 | This lets us get the desired value of 42, rather than `21*21`, `2*2`, etc. 104 | 105 | Note that `Prefix` is a named parameter, but we don't provide any 106 | inputs matching that name. In this case, argmapper by default falls back 107 | to treating it as a typed parameter, allowing our typed string input to 108 | match. 109 | 110 | ### Explicitly Typed Values 111 | 112 | The previous example showed `Prefix` implicitly using a typed-only 113 | match since there was no input named "Prefix". You can also explictly 114 | note that the name doesn't matter in two ways. 115 | 116 | First, you can use struct tags: 117 | 118 | ```go 119 | target, err := argmapper.NewFunc(func(input struct { 120 | // This tells argmapper to fill the values in this struct rather 121 | // than provide a value for the entire struct. 122 | argmapper.Struct 123 | 124 | A int 125 | B int 126 | Prefix string `argmapper:",typeOnly"` 127 | }) string { 128 | return fmt.Sprintf("%s: %d", in.Prefix, in.A*in.B) 129 | }) 130 | ``` 131 | 132 | You can also use a non-struct input. Go reflection doesn't reveal 133 | function parameter names so all function parameters are by definition 134 | type only: 135 | 136 | ```go 137 | target, err := argmapper.NewFunc(func(string) {}) 138 | ``` 139 | 140 | You can mix and match named and typed parameters. 141 | 142 | ### Conversion Function Chaining 143 | 144 | The example below shows how conversion functions are automatically 145 | chained as necessary to reach your desired function. 146 | 147 | ```go 148 | // Trivial function that takes a string and just returns it. 149 | target, err := argmapper.NewFunc(func(v string) string { return v }) 150 | 151 | result := target.Call( 152 | // "false" value 153 | argmapper.Typed(false), 154 | 155 | // bool to int 156 | argmapper.Converter(func(v bool) int { 157 | if v { 158 | return 1 159 | } 160 | 161 | return 0 162 | }), 163 | 164 | // int to string 165 | argmapper.Converter(func(v int) string { 166 | return strconv.Itoa(v) 167 | }), 168 | ) 169 | if result.Err() != nil { 170 | // If we didn't have converters necessary to get us from bool => int => string 171 | // then this would fail. 172 | panic(result.Err()) 173 | } 174 | 175 | // Prints "0" 176 | println(result.Out(0).(string)) 177 | ``` 178 | 179 | Typed converters preserve the name of their arguments. If the above input 180 | was `Named("foo", false)` rather than typed, then the name "foo" would 181 | be attached both the string and int values generated in case any target 182 | functions requested a named parameter. In the case of this example, the name 183 | is carried through but carries no consequence since the final target 184 | function is just a typed parameter. 185 | 186 | ### Conversion Function Cycles 187 | 188 | Cycles in conversion functions are completely allowed. The example 189 | below behaves as you would expect. This is a simple direct cycle, more complex 190 | cycles from chaining multiple converters will also behave correctly. This 191 | lets you register complex sets of bidirectional conversion functions with ease. 192 | 193 | ```go 194 | // Trivial function that takes a string and just returns it. 195 | target, err := argmapper.NewFunc(func(v string) string { return v }) 196 | 197 | result := target.Call( 198 | argmapper.Typed(12), 199 | argmapper.Converter(func(v int) string { return strconv.Itoa(v) }), 200 | argmapper.Converter(func(v string) (int, error) { return strconv.Atoi(v) }), 201 | ) 202 | if result.Err() != nil { 203 | // If we didn't have converters necessary to get us from bool => int => string 204 | // then this would fail. 205 | panic(result.Err()) 206 | } 207 | 208 | // Prints "12" 209 | println(result.Out(0).(string)) 210 | ``` 211 | 212 | ### Conversion Errors 213 | 214 | The example above has a converter that returns `(int, error)`. If the final 215 | return type of a converter is `error`, go-argmapper treats that as a special 216 | value signaling if the conversion succeeded or failed. 217 | 218 | If conversion fails, the target function call fails and the error is returned 219 | to the user. 220 | 221 | In the future, we plan on retrying via other possible conversion paths 222 | if they are available. 223 | -------------------------------------------------------------------------------- /args.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package argmapper 5 | 6 | import ( 7 | "errors" 8 | "reflect" 9 | "strings" 10 | 11 | "github.com/hashicorp/go-argmapper/internal/graph" 12 | "github.com/hashicorp/go-hclog" 13 | "github.com/hashicorp/go-multierror" 14 | ) 15 | 16 | // Arg is an option to Func.Call that sets the state for the function call. 17 | // This can be a direct named arg or a converter that could be used if 18 | // necessary to reach the target. 19 | type Arg func(*argBuilder) error 20 | 21 | type argBuilder struct { 22 | logger hclog.Logger 23 | named map[string]reflect.Value 24 | namedSub map[string]map[string]reflect.Value 25 | typed map[reflect.Type]reflect.Value 26 | typedSub map[reflect.Type]map[string]reflect.Value 27 | convs []*Func 28 | convGens []ConverterGenFunc 29 | 30 | redefining bool 31 | filterInput FilterFunc 32 | filterOutput FilterFunc 33 | 34 | funcName string 35 | funcOnce bool 36 | } 37 | 38 | func newArgBuilder(opts ...Arg) (*argBuilder, error) { 39 | builder := &argBuilder{ 40 | logger: hclog.L(), 41 | named: make(map[string]reflect.Value), 42 | namedSub: make(map[string]map[string]reflect.Value), 43 | typed: make(map[reflect.Type]reflect.Value), 44 | typedSub: make(map[reflect.Type]map[string]reflect.Value), 45 | } 46 | 47 | var buildErr error 48 | for _, opt := range opts { 49 | if opt == nil { 50 | return nil, errors.New("arg cannot be nil") 51 | } 52 | if err := opt(builder); err != nil { 53 | buildErr = multierror.Append(buildErr, err) 54 | } 55 | } 56 | 57 | return builder, buildErr 58 | } 59 | 60 | // Named specifies a named argument with the given value. This will satisfy 61 | // any requirement where the name matches AND the value is assignable to 62 | // the struct. 63 | // 64 | // If the name is an empty string, this is equivalent to calling Typed. 65 | func Named(n string, v interface{}) Arg { 66 | if n == "" { 67 | return Typed(v) 68 | } 69 | 70 | return func(a *argBuilder) error { 71 | rv := reflect.ValueOf(v) 72 | if !rv.IsValid() { 73 | return nil 74 | } 75 | 76 | a.named[strings.ToLower(n)] = rv 77 | return nil 78 | } 79 | } 80 | 81 | // NamedSubtype is the same as Named but specifies a subtype for the value. 82 | // 83 | // If the name is an empty string, this is the equivalent to calling TypedSubtype. 84 | func NamedSubtype(n string, v interface{}, st string) Arg { 85 | if n == "" { 86 | return TypedSubtype(v, st) 87 | } 88 | 89 | if st == "" { 90 | return Named(n, v) 91 | } 92 | 93 | return func(a *argBuilder) error { 94 | rv := reflect.ValueOf(v) 95 | if !rv.IsValid() { 96 | return nil 97 | } 98 | 99 | n = strings.ToLower(n) 100 | if a.namedSub[n] == nil { 101 | a.namedSub[n] = map[string]reflect.Value{} 102 | } 103 | a.namedSub[n][st] = rv 104 | return nil 105 | } 106 | } 107 | 108 | // Typed specifies a typed argument with the given value. This will satisfy 109 | // any requirement where the type is assignable to a required value. The name 110 | // can be anything of the required value. 111 | func Typed(vs ...interface{}) Arg { 112 | return func(a *argBuilder) error { 113 | for _, v := range vs { 114 | rv := reflect.ValueOf(v) 115 | if rv.IsValid() { 116 | a.typed[rv.Type()] = rv 117 | } 118 | } 119 | 120 | return nil 121 | } 122 | } 123 | 124 | // TypedSubtype is the same as Typed but specifies a subtype key for the value. 125 | // If the subtype is empty, this is equivalent to calling Typed. 126 | func TypedSubtype(v interface{}, st string) Arg { 127 | if st == "" { 128 | return Typed(v) 129 | } 130 | 131 | return func(a *argBuilder) error { 132 | rv := reflect.ValueOf(v) 133 | if !rv.IsValid() { 134 | return nil 135 | } 136 | 137 | rt := rv.Type() 138 | if a.typedSub[rt] == nil { 139 | a.typedSub[rt] = map[string]reflect.Value{} 140 | } 141 | a.typedSub[rt][st] = rv 142 | return nil 143 | } 144 | } 145 | 146 | // Converter specifies one or more converters to use if necessary. 147 | // A converter will be used if an argument type doesn't match exactly. 148 | func Converter(fs ...interface{}) Arg { 149 | return func(a *argBuilder) error { 150 | for _, f := range fs { 151 | conv, err := NewFunc(f) 152 | if err != nil { 153 | return err 154 | } 155 | 156 | a.convs = append(a.convs, conv) 157 | } 158 | 159 | return nil 160 | } 161 | } 162 | 163 | // ConverterFunc is the same as Converter but takes an already created 164 | // Func value. Any nil arguments are ignored. This appends to the list of 165 | // converters. 166 | func ConverterFunc(fs ...*Func) Arg { 167 | return func(a *argBuilder) error { 168 | for _, f := range fs { 169 | if f != nil { 170 | a.convs = append(a.convs, f) 171 | } 172 | } 173 | 174 | return nil 175 | } 176 | } 177 | 178 | // ConverterGenFunc is called with a value and should return a non-nil Func 179 | // if it is able to generate a converter on the fly based on this value. 180 | type ConverterGenFunc func(Value) (*Func, error) 181 | 182 | // ConverterGen registers a converter generator. A converter generator 183 | // generates a converter dynamically based on some set values. This can be 184 | // used to generate type conversions for example. The returned func can 185 | // have more requirements. 186 | // 187 | // If the function returns a nil Func, then no converter is generated. 188 | func ConverterGen(fs ...ConverterGenFunc) Arg { 189 | return func(a *argBuilder) error { 190 | a.convGens = append(a.convGens, fs...) 191 | return nil 192 | } 193 | } 194 | 195 | // FilterInput is used by Func.Redefine to define what inputs are valid. 196 | // This will replace any previously set FilterInput value. This has no effect 197 | // unless Func.Redefine is being called. 198 | func FilterInput(f FilterFunc) Arg { 199 | return func(a *argBuilder) error { 200 | a.filterInput = f 201 | return nil 202 | } 203 | } 204 | 205 | // FilterOutput is identical to FilterInput but for output values. If this 206 | // is not set, then Redefine will allow any output values. This behavior is 207 | // the same as if FilterInput were not specified. 208 | func FilterOutput(f FilterFunc) Arg { 209 | return func(a *argBuilder) error { 210 | a.filterOutput = f 211 | return nil 212 | } 213 | } 214 | 215 | // Logger specifies a logger to be used during operations with these 216 | // arguments. If this isn't specified, the default hclog.L() logger is used. 217 | func Logger(l hclog.Logger) Arg { 218 | return func(a *argBuilder) error { 219 | a.logger = l 220 | return nil 221 | } 222 | } 223 | 224 | // FuncName sets the function name. This is used only with NewFunc. 225 | func FuncName(n string) Arg { 226 | return func(a *argBuilder) error { 227 | a.funcName = n 228 | return nil 229 | } 230 | } 231 | 232 | // FuncOnce configures the function to be called at most once. The result of 233 | // a function call will be memoized and any future calls to the function 234 | // will return the memoized function. 235 | // 236 | // This is particularly useful if there is a complex converter that 237 | // may be required multiple times in a function call chain. 238 | // 239 | // The downside to this is that the result is memoized regardless of the 240 | // input arguments. Therefore, if the input arguments change, this function 241 | // will still not be called again. Users of this should be ABSOLUTELY SURE 242 | // that they want this function to run exactly once regardless of arguments 243 | // and return the same result every time. 244 | func FuncOnce() Arg { 245 | return func(a *argBuilder) error { 246 | a.funcOnce = true 247 | return nil 248 | } 249 | } 250 | 251 | func (b *argBuilder) graph(log hclog.Logger, g *graph.Graph, root graph.Vertex) ( 252 | []graph.Vertex, // input vertices 253 | []*Func, // converters 254 | ) { 255 | var result []graph.Vertex 256 | 257 | // Add our named inputs 258 | for k, v := range b.named { 259 | // Add the input 260 | input := g.AddOverwrite(&valueVertex{ 261 | Name: k, 262 | Type: v.Type(), 263 | Value: v, 264 | }) 265 | log.Trace("input", "kind", "named", "name", k, "type", v.Type(), "value", v) 266 | 267 | // Input depends on the input root 268 | g.AddEdge(input, root) 269 | 270 | // Track 271 | result = append(result, input) 272 | } 273 | 274 | // Add our named inputs with subtypes 275 | for k, m := range b.namedSub { 276 | for st, v := range m { 277 | // Add the input 278 | input := g.AddOverwrite(&valueVertex{ 279 | Name: k, 280 | Type: v.Type(), 281 | Subtype: st, 282 | Value: v, 283 | }) 284 | log.Trace("input", "kind", "named", "name", k, "value", v, "subtype", st) 285 | 286 | // Input depends on the input root 287 | g.AddEdge(input, root) 288 | 289 | // Track 290 | result = append(result, input) 291 | } 292 | } 293 | 294 | // Add our typed inputs 295 | for t, v := range b.typed { 296 | // Add the input 297 | input := g.AddOverwrite(&typedOutputVertex{ 298 | Type: t, 299 | Value: v, 300 | }) 301 | log.Trace("input", "kind", "typed", "type", t, "value", v) 302 | 303 | // Input depends on the input root 304 | g.AddEdge(input, root) 305 | 306 | // Track 307 | result = append(result, input) 308 | } 309 | 310 | // Add our typed inputs with subtypes 311 | for t, m := range b.typedSub { 312 | for st, v := range m { 313 | // Add the input 314 | input := g.AddOverwrite(&typedOutputVertex{ 315 | Type: t, 316 | Value: v, 317 | Subtype: st, 318 | }) 319 | log.Trace("input", "kind", "typed", "type", t, "value", v, "subtype", st) 320 | 321 | // Input depends on the input root 322 | g.AddEdge(input, root) 323 | 324 | // Track 325 | result = append(result, input) 326 | } 327 | } 328 | 329 | // If we have converters, add those. 330 | for _, f := range b.convs { 331 | f.graph(g, root, true) 332 | } 333 | 334 | // If we have converter generators, run those. 335 | convs := make([]*Func, len(b.convs)) 336 | copy(convs, b.convs) 337 | if len(b.convGens) > 0 { 338 | for _, vertex := range g.Vertices() { 339 | // Get a value. If this vertex can't be represented by a value, 340 | // then ignore it. 341 | value := newValueFromVertex(vertex) 342 | if value == nil { 343 | continue 344 | } 345 | 346 | // Go through each generator and create the converter. 347 | for _, gen := range b.convGens { 348 | f, err := gen(*value) 349 | if err != nil { 350 | // TODO: return 351 | panic(err) 352 | } 353 | if f == nil { 354 | continue 355 | } 356 | 357 | convs = append(convs, f) 358 | f.graph(g, root, true) 359 | } 360 | } 361 | } 362 | 363 | return result, convs 364 | } 365 | -------------------------------------------------------------------------------- /call.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package argmapper 5 | 6 | import ( 7 | "fmt" 8 | "reflect" 9 | 10 | "github.com/hashicorp/go-argmapper/internal/graph" 11 | "github.com/hashicorp/go-hclog" 12 | "github.com/hashicorp/go-multierror" 13 | ) 14 | 15 | // Call calls the function. Use the various Arg functions to set the state 16 | // for the function call. More details on how Call works are on the Func 17 | // struct documentation directly. 18 | func (f *Func) Call(opts ...Arg) Result { 19 | // Build up our args 20 | builder, buildErr := f.argBuilder(opts...) 21 | if buildErr != nil { 22 | return resultError(buildErr) 23 | } 24 | log := builder.logger 25 | log.Trace("call") 26 | 27 | // Build our call graph 28 | g, vertexRoot, vertexF, _, err := f.callGraph(builder) 29 | if err != nil { 30 | return resultError(err) 31 | } 32 | 33 | // Reach our target function to get our arguments, performing any 34 | // conversions necessary. 35 | argMap, err := f.reachTarget(log, &g, vertexRoot, vertexF, newCallState(), false) 36 | if err != nil { 37 | return resultError(err) 38 | } 39 | 40 | return f.callDirect(log, argMap) 41 | } 42 | 43 | // callGraph builds the common graph used by Call, Redefine, etc. 44 | func (f *Func) callGraph(args *argBuilder) ( 45 | g graph.Graph, 46 | vertexRoot graph.Vertex, 47 | vertexF graph.Vertex, 48 | vertexI []graph.Vertex, 49 | err error, 50 | ) { 51 | log := args.logger 52 | 53 | // Create a shared root. Anything reachable from the root is not pruned. 54 | // This is primarily inputs but may also contain parameterless converters 55 | // (providers). 56 | vertexRoot = g.Add(&rootVertex{}) 57 | 58 | // Build the graph. The first step is to add our function and all the 59 | // requirements of the function. We keep track of this in vertexF and 60 | // vertexT, respectively, because we'll need these later. 61 | vertexF = f.graph(&g, vertexRoot, false) 62 | vertexFreq := g.OutEdges(vertexF) 63 | 64 | // Next, we add "inputs", which are the given named values that 65 | // we already know about. These are tracked as "vertexI". 66 | var convs []*Func 67 | vertexI, convs = args.graph(log, &g, vertexRoot) 68 | 69 | // Next, for all values we may have or produce, we need to create 70 | // the vertices for the type-only value. This lets us say, for example, 71 | // that an input "A string" satisfies anything that requires only "string". 72 | for _, raw := range g.Vertices() { 73 | v, ok := raw.(*valueVertex) 74 | if !ok { 75 | continue 76 | } 77 | 78 | // We only add an edge from the output if we require a value. 79 | // If we already have a value then we don't need to request one. 80 | g.AddEdgeWeighted(v, g.Add(&typedOutputVertex{ 81 | Type: v.Type, 82 | }), weightTyped) 83 | 84 | // We always add an edge from the arg to the value, whether it 85 | // has one or not. In the next step, we'll prune any typed arguments 86 | // that already have a satisfied value. 87 | g.AddEdgeWeighted(g.Add(&typedArgVertex{ 88 | Type: v.Type, 89 | }), v, weightTyped) 90 | 91 | // If this value has a subtype, we add an edge for the subtype 92 | if v.Subtype != "" { 93 | g.AddEdgeWeighted(g.Add(&typedArgVertex{ 94 | Type: v.Type, 95 | Subtype: v.Subtype, 96 | }), v, weightTyped) 97 | } 98 | } 99 | 100 | // We need to allow any typed argument to depend on a typed output. 101 | // This lets two converters chain together. 102 | for _, raw := range g.Vertices() { 103 | v, ok := raw.(*typedArgVertex) 104 | if !ok { 105 | continue 106 | } 107 | 108 | g.AddEdgeWeighted(v, g.Add(&typedOutputVertex{ 109 | Type: v.Type, 110 | Subtype: v.Subtype, 111 | }), weightTyped) 112 | } 113 | 114 | // Typed output vertices that are interfaces can be satisfied by 115 | // interface implementations. i.e. `out: error` -> `out: *fmt.Error`. 116 | for _, raw := range g.Vertices() { 117 | v, ok := raw.(*typedOutputVertex) 118 | if !ok || v.Type.Kind() != reflect.Interface { 119 | continue 120 | } 121 | 122 | for _, raw2 := range g.Vertices() { 123 | if raw == raw2 { 124 | continue 125 | } 126 | 127 | v2, ok := raw2.(*typedOutputVertex) 128 | if !ok || !v2.Type.Implements(v.Type) { 129 | continue 130 | } 131 | 132 | g.AddEdgeWeighted(v, v2, weightTyped) 133 | } 134 | } 135 | 136 | // All named values that have no subtype can take a value from 137 | // any other named value that has a subtype. 138 | for _, raw := range g.Vertices() { 139 | v, ok := raw.(*valueVertex) 140 | if !ok || v.Subtype != "" || v.Value.IsValid() { 141 | continue 142 | } 143 | 144 | for _, raw := range g.Vertices() { 145 | v2, ok := raw.(*valueVertex) 146 | if !ok || v2.Type != v.Type || v2.Subtype == "" { 147 | continue 148 | } 149 | 150 | g.AddEdgeWeighted(v, v2, weightTyped) 151 | } 152 | } 153 | 154 | // All typed values that have no subtype can take a value from 155 | // any output with a subtype. 156 | for _, raw := range g.Vertices() { 157 | v, ok := raw.(*typedArgVertex) 158 | if !ok || v.Subtype != "" { 159 | continue 160 | } 161 | 162 | for _, raw := range g.Vertices() { 163 | v2, ok := raw.(*typedOutputVertex) 164 | if !ok || v2.Type != v.Type || v2.Subtype == "" { 165 | continue 166 | } 167 | 168 | g.AddEdgeWeighted(v, v2, weightTypedOtherSubtype) 169 | } 170 | } 171 | 172 | // All typed values that have no subtype can take a value from 173 | // any output with a subtype. 174 | for _, raw := range g.Vertices() { 175 | v, ok := raw.(*typedArgVertex) 176 | if !ok || v.Subtype == "" { 177 | continue 178 | } 179 | 180 | for _, raw := range g.Vertices() { 181 | v2, ok := raw.(*typedOutputVertex) 182 | if !ok || v2.Type != v.Type || v2.Subtype != "" { 183 | continue 184 | } 185 | 186 | g.AddEdgeWeighted(v, v2, weightTypedOtherSubtype) 187 | } 188 | } 189 | 190 | // If we're redefining based on inputs, then we also want to 191 | // go through and set a path from our input root to all the values 192 | // in the graph. This lets us pick the shortest path through based on 193 | // any valid input. 194 | if args.redefining { 195 | for _, raw := range g.Vertices() { 196 | var value Value 197 | 198 | // We are looking for either a value or a typed arg. Both 199 | // of these represent "inputs" to a function. 200 | v, ok := raw.(*valueVertex) 201 | if ok { 202 | value = Value{ 203 | Name: v.Name, 204 | Type: v.Type, 205 | Subtype: v.Subtype, 206 | Value: v.Value, 207 | } 208 | } 209 | if !ok { 210 | v, ok := raw.(*typedArgVertex) 211 | if !ok { 212 | continue 213 | } 214 | 215 | value = Value{ 216 | Type: v.Type, 217 | Subtype: v.Subtype, 218 | Value: v.Value, 219 | } 220 | } 221 | 222 | // For redefining, the caller can setup filters to determine 223 | // what inputs they're capable of providing. If any filter 224 | // says it is possible, then we take the value. 225 | include := true 226 | if args.filterInput != nil && !args.filterInput(value) { 227 | log.Trace("excluding input due to failed filter", "value", value) 228 | continue 229 | } 230 | 231 | if include { 232 | // Connect this to the root, since it is a potential input to 233 | // satisfy a function that gets us to redefine. 234 | g.AddEdge(raw, vertexRoot) 235 | } 236 | } 237 | } 238 | 239 | log.Trace("full graph (may have cycles)", "graph", g.String()) 240 | 241 | // Next we do a DFS from each input A in I to the function F. 242 | // This gives us the full set of reachable nodes from our inputs 243 | // and at most to F. Using this information, we can prune any nodes 244 | // that are guaranteed to be unused. 245 | // 246 | // DFS from the input root and record what we see. We have to reverse the 247 | // graph here because we typically have out edges pointing to 248 | // requirements, but we're going from requirements (inputs) to 249 | // the function. 250 | visited := map[interface{}]struct{}{ 251 | // We must keep the root. Since we're starting from the root we don't 252 | // "visit" it. But we must keep it for shortest path calculations. If 253 | // we don't keep it, our shortest path calculations are from some 254 | // other zero index topo sort value. 255 | graph.VertexID(vertexRoot): struct{}{}, 256 | } 257 | _ = g.Reverse().DFS(vertexRoot, func(v graph.Vertex, next func() error) error { 258 | visited[graph.VertexID(v)] = struct{}{} 259 | 260 | if v == vertexF { 261 | return nil 262 | } 263 | return next() 264 | }) 265 | 266 | // Remove all the non-visited vertices. After this, what we'll have 267 | // is a graph that has many paths getting us from inputs to function, 268 | // but we will have no spurious vertices that are unreachable from our 269 | // inputs. 270 | for _, v := range g.Vertices() { 271 | if _, ok := visited[graph.VertexID(v)]; !ok { 272 | g.Remove(v) 273 | } 274 | } 275 | log.Trace("graph after input DFS", "graph", g.String()) 276 | 277 | // Go through all our inputs. If any aren't in the graph any longer 278 | // it means there is no possible path to that input so it cannot be 279 | // satisfied. 280 | var unsatisfied []*Value 281 | for _, req := range vertexFreq { 282 | if g.Vertex(graph.VertexID(req)) == nil { 283 | valueable, ok := req.(valueConverter) 284 | if !ok { 285 | // This shouldn't be possible 286 | panic(fmt.Sprintf("argmapper graph node doesn't implement value(): %T", req)) 287 | } 288 | 289 | unsatisfied = append(unsatisfied, valueable.value()) 290 | } 291 | } 292 | 293 | // If we have unsatisfied inputs, then put together the data we need to 294 | // build our error result and return it. 295 | if len(unsatisfied) > 0 { 296 | // Build our list of direct inputs 297 | var inputs []*Value 298 | for _, v := range vertexI { 299 | valueable, ok := v.(valueConverter) 300 | if !ok { 301 | // This shouldn't be possible 302 | panic(fmt.Sprintf("argmapper graph node doesn't implement value(): %T", v)) 303 | } 304 | 305 | inputs = append(inputs, valueable.value()) 306 | } 307 | 308 | err = &ErrArgumentUnsatisfied{ 309 | Func: f, 310 | Args: unsatisfied, 311 | Inputs: inputs, 312 | Converters: convs, 313 | } 314 | return 315 | } 316 | 317 | return 318 | } 319 | 320 | // reachTarget executes the given funcVertex by ensuring we satisfy 321 | // all the inbound arguments first and then calling it. 322 | func (f *Func) reachTarget( 323 | log hclog.Logger, 324 | g *graph.Graph, 325 | root graph.Vertex, 326 | target graph.Vertex, 327 | state *callState, 328 | redefine bool, 329 | ) (map[interface{}]reflect.Value, error) { 330 | log.Trace("reachTarget", "target", target) 331 | 332 | // argMap will store all the values that this target depends on. 333 | argMap := map[interface{}]reflect.Value{} 334 | 335 | // Look at the out edges, since these are the requirements for the conv 336 | // and determine which inputs we need values for. If we have a value 337 | // already then we skip the target because we assume it is already in 338 | // the state. 339 | var vertexT []graph.Vertex 340 | for _, out := range g.OutEdges(target) { 341 | skip := false 342 | switch v := out.(type) { 343 | case *rootVertex: 344 | // If we see a root vertex, then that means that this target 345 | // has no dependencies. 346 | skip = true 347 | 348 | case *typedArgVertex: 349 | if v.Value.IsValid() { 350 | skip = true 351 | argMap[graph.VertexID(out)] = v.Value 352 | } 353 | } 354 | 355 | // If we're skipping because we have this value already, then 356 | // note that we're using this input in the input set. 357 | if skip { 358 | state.InputSet[graph.VertexID(out)] = out 359 | continue 360 | } 361 | 362 | log.Trace("conv is missing an input", "input", out) 363 | vertexT = append(vertexT, out) 364 | } 365 | 366 | if len(vertexT) == 0 { 367 | log.Trace("conv satisfied") 368 | return argMap, nil 369 | } 370 | 371 | // It is possible that we detect unsatisfied values here. This is rare 372 | // since the initial graph build catches most cases, but in advanced 373 | // Waypoint usage it happens here. 374 | var unsatisfied []*Value 375 | 376 | paths := make([][]graph.Vertex, len(vertexT)) 377 | for i, current := range vertexT { 378 | currentG := g 379 | 380 | // For value vertices, we discount any other values that share the 381 | // same name. This lets our shortest paths prefer matching through 382 | // same-named arguments. 383 | if currentValue, ok := current.(*valueVertex); ok { 384 | currentG = currentG.Copy() 385 | for _, raw := range currentG.Vertices() { 386 | if v, ok := raw.(*valueVertex); ok && v.Name == currentValue.Name { 387 | for _, src := range currentG.InEdges(raw) { 388 | currentG.AddEdgeWeighted(src, raw, weightMatchingName) 389 | } 390 | } 391 | } 392 | } 393 | 394 | // Recalculate the shortest path information since we may changed 395 | // the graph above. 396 | _, edgeTo := currentG.Reverse().Dijkstra(root) 397 | 398 | // With the latest shortest paths, let's add the path for this target. 399 | paths[i] = currentG.EdgeToPath(current, edgeTo) 400 | log.Trace("path for target", "target", current, "path", paths[i]) 401 | 402 | // Get the input 403 | input := paths[i][0] 404 | if _, ok := input.(*rootVertex); ok && len(paths[i]) > 1 { 405 | input = paths[i][1] 406 | } 407 | 408 | // If the path contains ourself, then this target is unsatisfied. 409 | for _, v := range paths[i] { 410 | if v == target { 411 | valueable, ok := current.(valueConverter) 412 | if !ok { 413 | // This shouldn't be possible 414 | panic(fmt.Sprintf("argmapper graph node doesn't implement value(): %T", current)) 415 | } 416 | 417 | unsatisfied = append(unsatisfied, valueable.value()) 418 | } 419 | } 420 | 421 | // Store our input used 422 | state.InputSet[graph.VertexID(input)] = input 423 | 424 | // When we're redefining, we always set the initial input to 425 | // the zero value because we assume we'll have access to it. We 426 | // can assume this because that is the whole definition of redefining. 427 | if redefine { 428 | switch v := input.(type) { 429 | case *valueVertex: 430 | if !v.Value.IsValid() { 431 | v.Value = reflect.Zero(v.Type) 432 | } 433 | 434 | case *typedArgVertex: 435 | v.Value = reflect.Zero(v.Type) 436 | } 437 | } 438 | } 439 | 440 | // If we have any unsatisfied values, error. 441 | if len(unsatisfied) > 0 { 442 | return nil, &ErrArgumentUnsatisfied{ 443 | Func: f, 444 | Args: unsatisfied, 445 | 446 | // NOTE(mitchellh): This doesn't populate Inputs and Convs 447 | // which is theoretically possible but a lot more difficult. 448 | // Given this error is relatively rare, we can tackle this later. 449 | } 450 | } 451 | 452 | // Go through each path 453 | for _, path := range paths { 454 | // finalValue will be set to our final value that we see when walking. 455 | // This will be set as the value for this required input. 456 | var finalValue reflect.Value 457 | 458 | for pathIdx, vertex := range path { 459 | log.Trace("executing node", "current", vertex) 460 | switch v := vertex.(type) { 461 | case *rootVertex: 462 | // Do nothing 463 | 464 | case *valueVertex: 465 | // Store the last viewed vertex in our path state 466 | state.Value = v.Value 467 | 468 | if pathIdx > 0 { 469 | prev := path[pathIdx-1] 470 | if r, ok := prev.(*typedOutputVertex); ok { 471 | log.Trace("setting node value", "value", r.Value) 472 | v.Value = r.Value 473 | } 474 | } 475 | 476 | // If we have a valid value set, then put it on our named list. 477 | if v.Value.IsValid() { 478 | state.NamedValue[v.Name] = v.Value 479 | 480 | finalValue = v.Value 481 | } 482 | 483 | case *typedArgVertex: 484 | // If we have a value set on the state then we set that to this 485 | // value. This is true in every Call case but is always false 486 | // for Redefine. 487 | if state.Value.IsValid() && state.Value.Type().AssignableTo(v.Type) { 488 | // The value of this is the last value vertex we saw. The graph 489 | // walk should ensure this is the correct type. 490 | v.Value = state.Value 491 | } 492 | 493 | // Setup our mapping so that we know that this wildcard 494 | // maps to this name. 495 | state.TypedValue[v.Type] = v.Value 496 | 497 | finalValue = v.Value 498 | 499 | case *typedOutputVertex: 500 | // If our last node was another typed output, then we take 501 | // that value. 502 | if pathIdx > 0 { 503 | prev := path[pathIdx-1] 504 | if r, ok := prev.(*typedOutputVertex); ok { 505 | log.Trace("setting node value", "value", r.Value) 506 | v.Value = r.Value 507 | } 508 | } 509 | 510 | // Last value 511 | state.Value = v.Value 512 | 513 | // Set the typed value we can read from. 514 | state.TypedValue[v.Type] = v.Value 515 | 516 | case *funcVertex: 517 | // Reach our arguments if they aren't already. 518 | funcArgMap, err := f.reachTarget( 519 | log, //log.Named(graph.VertexName(v)), 520 | g, 521 | root, 522 | v, 523 | state, 524 | redefine, 525 | ) 526 | if err != nil { 527 | return nil, err 528 | } 529 | 530 | // Call our function. 531 | result := v.Func.callDirect(log, funcArgMap) 532 | if err := result.Err(); err != nil { 533 | return nil, err 534 | } 535 | 536 | // Update our graph nodes and continue 537 | v.Func.outputValues(result, g.InEdges(v), state) 538 | 539 | default: 540 | panic(fmt.Sprintf("unknown vertex: %v", v)) 541 | } 542 | } 543 | 544 | // We should always have a final value, because our execution to 545 | // this point only leads up to this value. 546 | if !finalValue.IsValid() { 547 | panic(fmt.Sprintf("didn't reach a final value for path: %#v", path)) 548 | } 549 | 550 | // We store the final value in the input map. 551 | log.Trace("final value", "vertex", path[len(path)-1], "value", finalValue.Interface()) 552 | argMap[graph.VertexID(path[len(path)-1])] = finalValue 553 | } 554 | 555 | // Reached our goal 556 | return argMap, nil 557 | } 558 | 559 | // call -- the unexported version of Call -- calls the function directly 560 | // with the given named arguments. This skips the whole graph creation 561 | // step by requiring args satisfy all required arguments. 562 | func (f *Func) callDirect(log hclog.Logger, argMap map[interface{}]reflect.Value) Result { 563 | // If we have FuncOnce enabled and we've been called before, return 564 | // the result we have cached. 565 | if f.once && f.onceResult != nil { 566 | log.Trace("returning cached result, FuncOnce enabled") 567 | return *f.onceResult 568 | } 569 | 570 | // Initialize the struct we'll be populating 571 | var buildErr error 572 | structVal := f.input.newStructValue() 573 | for _, val := range f.input.values { 574 | arg, ok := argMap[graph.VertexID(val.vertex())] 575 | if !ok { 576 | // This should never happen because we catch unsatisfied errors 577 | // earlier in the process. Because of this, we output a message 578 | // that there is a bug. 579 | //nolint:staticcheck // ST1005 Consumers maybe relying on the error message. 580 | buildErr = multierror.Append(buildErr, fmt.Errorf( 581 | "argument cannot be satisfied: %s. This is a bug in "+ 582 | "the go-argmapper library since this shouldn't happen at this "+ 583 | "point.", val.String())) 584 | continue 585 | } 586 | 587 | structVal.Field(val.index).Set(arg) 588 | } 589 | 590 | // If there was an error setting up the struct, then report that. 591 | if buildErr != nil { 592 | return Result{buildErr: buildErr} 593 | } 594 | 595 | // Call our function 596 | in := structVal.CallIn() 597 | for i, arg := range in { 598 | log.Trace("argument", "idx", i, "value", arg.Interface()) 599 | } 600 | 601 | out := f.fn.Call(in) 602 | result := Result{out: out} 603 | 604 | // If we have FuncOnce enabled, cache the result. 605 | if f.once { 606 | f.onceResult = &result 607 | } 608 | 609 | return result 610 | } 611 | 612 | // callState is the shared state for the execution of a single call. 613 | type callState struct { 614 | // NamedValue holds the current table of known named values. 615 | NamedValue map[string]reflect.Value 616 | 617 | // TypedValue holds the current table of assigned typed values. 618 | TypedValue map[reflect.Type]reflect.Value 619 | 620 | // Value is the last seen value vertex. This state is preserved so 621 | // we can set the typedVertex values properly. 622 | Value reflect.Value 623 | 624 | // TODO 625 | InputSet map[interface{}]graph.Vertex 626 | } 627 | 628 | func newCallState() *callState { 629 | return &callState{ 630 | NamedValue: map[string]reflect.Value{}, 631 | TypedValue: map[reflect.Type]reflect.Value{}, 632 | InputSet: map[interface{}]graph.Vertex{}, 633 | } 634 | } 635 | -------------------------------------------------------------------------------- /convert.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package argmapper 5 | 6 | import "reflect" 7 | 8 | // Convert converts the input arguments to the given target type. Convert will 9 | // use any of the available arguments and converters to reach the given target 10 | // type. 11 | func Convert(target reflect.Type, opts ...Arg) (interface{}, error) { 12 | out, err := convertMulti([]reflect.Type{target}, opts...) 13 | if err != nil { 14 | return nil, err 15 | } 16 | 17 | return out[0].Interface(), nil 18 | } 19 | 20 | // convertMulti is the same as Convert but converts multiple types at one 21 | // time. This is not currently exposed publicly since it is a slightly more 22 | // complicated interfaces and callers can call Convert multiple times. But 23 | // we use this internally for Redefine and FilterOutput. 24 | func convertMulti(target []reflect.Type, opts ...Arg) ([]reflect.Value, error) { 25 | // The way we get convert to work is that we make a dynamic function 26 | // that takes the target type as input, and then call Call on it. This 27 | // lets our DI system automatically determine our conversion. 28 | f, err := convertFunc(target) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | // Call it 34 | result := f.Call(opts...) 35 | if err := result.Err(); err != nil { 36 | return nil, err 37 | } 38 | 39 | // Our result is the first result 40 | return result.out, nil 41 | } 42 | 43 | // convertFunc constructs a function that takes values of the given target 44 | // and returns them directly. We use this for conversion. 45 | func convertFunc(target []reflect.Type) (*Func, error) { 46 | funcType := reflect.FuncOf(target, target, false) 47 | funcVal := reflect.MakeFunc(funcType, func(args []reflect.Value) []reflect.Value { 48 | return args 49 | }) 50 | 51 | // Create our "Func" type from our newly created function. 52 | return NewFunc(funcVal.Interface()) 53 | } 54 | -------------------------------------------------------------------------------- /convert_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package argmapper 5 | 6 | import ( 7 | "reflect" 8 | "strconv" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestConvert(t *testing.T) { 15 | cases := []struct { 16 | Name string 17 | Args []Arg 18 | Target interface{} 19 | Expected interface{} 20 | }{ 21 | { 22 | "primitive to primitive", 23 | []Arg{ 24 | Typed("42"), 25 | Converter(func(v string) (int, error) { return strconv.Atoi(v) }), 26 | }, 27 | (*int)(nil), 28 | int(42), 29 | }, 30 | 31 | { 32 | "primitive to interface type", 33 | []Arg{ 34 | Typed("42"), 35 | Converter(func(v string) testInterface { return &testInterfaceImpl{} }), 36 | }, 37 | (*testInterface)(nil), 38 | &testInterfaceImpl{}, 39 | }, 40 | 41 | { 42 | "primitive to interface implementation", 43 | []Arg{ 44 | Typed("42"), 45 | Converter(func(v string) *testInterfaceImpl { return &testInterfaceImpl{} }), 46 | }, 47 | (*testInterface)(nil), 48 | &testInterfaceImpl{}, 49 | }, 50 | } 51 | 52 | for _, tt := range cases { 53 | t.Run(tt.Name, func(t *testing.T) { 54 | require := require.New(t) 55 | 56 | result, err := Convert(reflect.TypeOf(tt.Target).Elem(), tt.Args...) 57 | require.NoError(err) 58 | require.Equal(tt.Expected, result) 59 | }) 60 | } 61 | } 62 | 63 | type testInterface interface { 64 | error 65 | } 66 | 67 | type testInterfaceImpl struct{} 68 | 69 | func (*testInterfaceImpl) Error() string { return "hello" } 70 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package argmapper is a dependency-injection library for Go. 5 | // 6 | // go-argmapper supports named values, typed values, automatically chaining 7 | // conversion functions to reach desired types, and more. go-argmapper is 8 | // designed for runtime, reflection-based dependency injection. 9 | // 10 | // The primary usage of this library is via the Func struct. See Func for 11 | // more documentation. 12 | package argmapper 13 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package argmapper 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "strings" 10 | ) 11 | 12 | // ErrArgumentUnsatisfied is the value returned when there is an argument 13 | // to a target function that cannot be satisfied given the inputs and 14 | // mappers. 15 | type ErrArgumentUnsatisfied struct { 16 | // Func is the target function call that was attempted. 17 | Func *Func 18 | 19 | // Args are the args that aren't satisfied. Note that this won't have 20 | // the "Value" field set because an unsatisfied argument by definition 21 | // is missing a value. 22 | Args []*Value 23 | 24 | // Inputs is the list of values that were provided directly to the 25 | // function call that we could use to populate arguments. 26 | Inputs []*Value 27 | 28 | // Converters is the list of converter functions available for use. 29 | Converters []*Func 30 | } 31 | 32 | func (e *ErrArgumentUnsatisfied) Error() string { 33 | // Build our list of arguments the function expects 34 | fullArg := new(bytes.Buffer) 35 | for _, arg := range e.Func.Input().Values() { 36 | fmt.Fprintf(fullArg, " - %s\n", arg.String()) 37 | } 38 | 39 | // Build our list of missing arguments 40 | missing := new(bytes.Buffer) 41 | for _, arg := range e.Args { 42 | fmt.Fprintf(missing, " - %s\n", arg.String()) 43 | } 44 | 45 | // Build our list of missing arguments 46 | inputs := new(bytes.Buffer) 47 | if len(e.Inputs) == 0 { 48 | fmt.Fprintf(inputs, " No inputs!\n") 49 | } 50 | for _, arg := range e.Inputs { 51 | fmt.Fprintf(inputs, " - %s\n", arg.String()) 52 | } 53 | 54 | convs := new(bytes.Buffer) 55 | if len(e.Converters) == 0 { 56 | fmt.Fprintf(convs, " No converter functions.\n") 57 | } 58 | for _, conv := range e.Converters { 59 | fmt.Fprintf(convs, " - %s\n", conv.Name()) 60 | for _, arg := range conv.Input().Values() { 61 | fmt.Fprintf(convs, " > %s\n", arg.String()) 62 | } 63 | for _, arg := range conv.Output().Values() { 64 | fmt.Fprintf(convs, " < %s\n", arg.String()) 65 | } 66 | } 67 | 68 | return fmt.Sprintf(` 69 | Argument to function %q could not be satisfied! 70 | 71 | This means that one (or more) of the arguments to a function do not 72 | have values that could be populated. A complete error description is below 73 | for debugging. 74 | 75 | ==> Unsatisfiable arguments 76 | This is a list of the arguments that a value could not be found. 77 | 78 | %s 79 | 80 | ==> Full list of desired function arguments 81 | This is a list of the arguments the function expected. Some arguments 82 | are named and some are unnamed. Unnamed arguments are matched by type. 83 | 84 | %s 85 | 86 | ==> Full list of direct inputs 87 | This is a list of the direct inputs that were available. None of these 88 | matched the unsatisfied arguments. Note that inputs are also possible 89 | through mappers, listed after this section. 90 | 91 | %s 92 | 93 | ==> Full list of available converters 94 | This is the list of functions that can be used to convert direct 95 | inputs (possibly being called in a chain) into a desired function 96 | argument. Arguments prefixed with ">" are inputs and arguments prefixed 97 | with "<" are outputs. 98 | 99 | %s 100 | `, 101 | e.Func.Name(), 102 | strings.TrimSuffix(missing.String(), "\n"), 103 | strings.TrimSuffix(fullArg.String(), "\n"), 104 | strings.TrimSuffix(inputs.String(), "\n"), 105 | strings.TrimSuffix(convs.String(), "\n"), 106 | ) 107 | } 108 | 109 | var _ error = (*ErrArgumentUnsatisfied)(nil) 110 | -------------------------------------------------------------------------------- /filter.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package argmapper 5 | 6 | import "reflect" 7 | 8 | type FilterFunc func(Value) bool 9 | 10 | // FilterType filters values based on matching the given type. If the type 11 | // is an interface value then any types that implement the interface will 12 | // also pass. 13 | func FilterType(t reflect.Type) FilterFunc { 14 | return func(v Value) bool { 15 | // Direct match is always true 16 | if v.Type == t { 17 | return true 18 | } 19 | 20 | // If our type is an interface and the value type implements it, true 21 | return t.Kind() == reflect.Interface && v.Type.Implements(t) 22 | } 23 | } 24 | 25 | // FilterOr returns a FilterFunc that returns true if any of the given 26 | // filter functions return true. 27 | func FilterOr(fs ...FilterFunc) FilterFunc { 28 | return func(v Value) bool { 29 | for _, f := range fs { 30 | if f(v) { 31 | return true 32 | } 33 | } 34 | 35 | return false 36 | } 37 | } 38 | 39 | // FilterAnd returns a FilterFunc that returns true if any of the given 40 | // filter functions return true. 41 | func FilterAnd(fs ...FilterFunc) FilterFunc { 42 | return func(v Value) bool { 43 | for _, f := range fs { 44 | if !f(v) { 45 | return false 46 | } 47 | } 48 | 49 | return true 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /func.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package argmapper 5 | 6 | import ( 7 | "fmt" 8 | "reflect" 9 | "runtime" 10 | 11 | "github.com/hashicorp/go-argmapper/internal/graph" 12 | ) 13 | 14 | // Func represents both a target function you want to execute as well as 15 | // a function that can be used to provide values, convert types, etc. for 16 | // calling another Func. 17 | // 18 | // A Func can take any number of arguments and return any number of values. 19 | // Direct function arguments are matched via type. You may use a struct 20 | // that embeds the Struct type (see Struct) for named value matching. 21 | // Go reflection doesn't enable accessing direct function parameter names, 22 | // so a struct is required for named matching. 23 | // 24 | // Structs that do not embed the Struct type are matched as typed. 25 | // 26 | // Converter Basics 27 | // 28 | // A Func also can act as a converter for another function call when used 29 | // with the Converter Arg option. 30 | // 31 | // Converters are used if a direct match argument isn't found for a Func call. 32 | // If a converter exists (or a chain of converts) to go from the input arguments 33 | // to the desired argument, then the chain will be called and the result used. 34 | // 35 | // Like any typical Func, converters can take as input zero or more values of 36 | // any kind. Converters can return any number of values as a result. Note that 37 | // while no return values are acceptable, such a converter would never be 38 | // called since it provides no value to the target function call. 39 | // 40 | // Converters can output both typed and named values. Similar to inputs, 41 | // outputting a name value requires using a struct with the Struct type 42 | // embedded. 43 | // 44 | // Converter Errors 45 | // 46 | // A final return type of "error" can be used with converters to signal 47 | // that conversion failed. If this occurs, the full function call attempt 48 | // fails and the error is reported to the user. 49 | // 50 | // If there is only one return value and it is of type "error", then this 51 | // is still considered the error result. A function can't return a non-erroneous 52 | // error value without returning more than one result value. 53 | // 54 | // Converter Priorities 55 | // 56 | // When multiple converters are available to reach some desired type, 57 | // Func will determine which converter to call using an implicit "cost" 58 | // associated with the converter. The cost is calculated across multiple 59 | // dimensions: 60 | // 61 | // * When converting from one named value to another, such as "Input int" 62 | // to "Input string", conversion will favor any converters that explicitly 63 | // use the equivalent name (but different type). So if there are two 64 | // converters, one `func(int) string` and another `func(Input int) string`, 65 | // then the latter will be preferred. 66 | // 67 | // * Building on the above, if there is only one converter `func(int) string` 68 | // but there are multiple `int` inputs available, an input with a matching 69 | // name is preferred. Therefore, if an input named `Input` is available, 70 | // that will be used for the conversion. 71 | // 72 | // * Converters that have less input values are preferred. This isn't 73 | // a direct parameter count on the function, but a count on the input 74 | // values which includes struct members and so on. 75 | // 76 | type Func struct { 77 | fn reflect.Value 78 | input *ValueSet 79 | output *ValueSet 80 | callOpts []Arg 81 | name string 82 | once bool 83 | onceResult *Result 84 | } 85 | 86 | // MustFunc can be called around NewFunc in order to force success and 87 | // panic if there is any error. 88 | func MustFunc(f *Func, err error) *Func { 89 | if err != nil { 90 | panic(err) 91 | } 92 | 93 | return f 94 | } 95 | 96 | // NewFunc creates a new Func from the given input function f. 97 | // 98 | // For more details on the format of the function f, see the package docs. 99 | // 100 | // Additional opts can be provided. These will always be set when calling 101 | // Call. Any conflicting arguments given on Call will override these args. 102 | // This can be used to provide some initial values, converters, etc. 103 | func NewFunc(f interface{}, opts ...Arg) (*Func, error) { 104 | args, err := newArgBuilder(opts...) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | fv := reflect.ValueOf(f) 110 | ft := fv.Type() 111 | if k := ft.Kind(); k != reflect.Func { 112 | return nil, fmt.Errorf("fn should be a function, got %s", k) 113 | } 114 | 115 | inTyp, err := newValueSet(ft.NumIn(), ft.In) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | // Get our output parameters. If the last parameter is an error type 121 | // then we don't parse that as the struct information. 122 | numOut := ft.NumOut() 123 | if numOut >= 1 && ft.Out(numOut-1) == errType { 124 | numOut -= 1 125 | } 126 | 127 | outTyp, err := newValueSet(numOut, ft.Out) 128 | if err != nil { 129 | return nil, err 130 | } 131 | 132 | return &Func{ 133 | fn: fv, 134 | input: inTyp, 135 | output: outTyp, 136 | callOpts: opts, 137 | name: args.funcName, 138 | once: args.funcOnce, 139 | }, nil 140 | } 141 | 142 | // NewFuncList initializes multiple Funcs at once. This is the same as 143 | // calling NewFunc for each f. 144 | func NewFuncList(fs []interface{}, opts ...Arg) ([]*Func, error) { 145 | result := make([]*Func, len(fs)) 146 | for i, f := range fs { 147 | var err error 148 | result[i], err = NewFunc(f) 149 | if err != nil { 150 | return nil, err 151 | } 152 | } 153 | 154 | return result, nil 155 | } 156 | 157 | // BuildFunc builds a function based on the specified input and output 158 | // value sets. When called, this will call the cb with a valueset matching 159 | // input and output with the argument values set. The cb should return 160 | // a populated ValueSet. 161 | func BuildFunc(input, output *ValueSet, cb func(in, out *ValueSet) error, opts ...Arg) (*Func, error) { 162 | // If our input or output is nil, we set it to an allocated empty value set. 163 | // This lets the caller always have a non-nil value but it may contain 164 | // no values. 165 | if input == nil { 166 | input = &ValueSet{} 167 | } 168 | if output == nil { 169 | output = &ValueSet{} 170 | } 171 | 172 | // Make our function type. 173 | funcType := reflect.FuncOf( 174 | input.Signature(), 175 | append(output.Signature(), errType), // append error so we can return errors 176 | false, 177 | ) 178 | 179 | // Build our function 180 | return NewFunc(reflect.MakeFunc(funcType, func(vs []reflect.Value) []reflect.Value { 181 | // Set our input 182 | if err := input.FromSignature(vs); err != nil { 183 | // FromSignature can not currently return an error 184 | panic(err) 185 | } 186 | // Call 187 | if err := cb(input, output); err != nil { 188 | return append(output.SignatureValues(), reflect.ValueOf(err)) 189 | } 190 | 191 | return append(output.SignatureValues(), reflect.Zero(errType)) 192 | }).Interface(), opts...) 193 | } 194 | 195 | // Input returns the input ValueSet for this function, representing the values 196 | // that this function requires as input. 197 | func (f *Func) Input() *ValueSet { return f.input } 198 | 199 | // Output returns the output ValueSet for this function, representing the values 200 | // that this function produces as an output. 201 | func (f *Func) Output() *ValueSet { return f.output } 202 | 203 | // Func returns the function pointer that this Func is built around. 204 | func (f *Func) Func() interface{} { 205 | return f.fn.Interface() 206 | } 207 | 208 | // Name returns the name of the function. 209 | // 210 | // This will return the configured name if one was given on NewFunc. If not, 211 | // this will attempt to look up the function name using the pointer. If 212 | // no friendly name can be found, then this will default to the function 213 | // type signature. 214 | func (f *Func) Name() string { 215 | // Use our set name first, if we have one 216 | name := f.name 217 | 218 | // Fall back to inspecting the program counter 219 | if name == "" { 220 | if rfunc := runtime.FuncForPC(f.fn.Pointer()); rfunc != nil { 221 | name = rfunc.Name() 222 | } 223 | 224 | // Final fallback is our type signature 225 | if name == "" { 226 | name = f.fn.String() 227 | } 228 | } 229 | 230 | return name 231 | } 232 | 233 | // String returns the name for this function. See Name. 234 | func (f *Func) String() string { 235 | return f.Name() 236 | } 237 | 238 | // argBuilder returns the instantiated argBuilder based on the opts provided 239 | // as well as the default opts attached to the func. 240 | func (f *Func) argBuilder(opts ...Arg) (*argBuilder, error) { 241 | if len(f.callOpts) > 0 { 242 | optsCopy := make([]Arg, len(opts)+len(f.callOpts)) 243 | copy(optsCopy, f.callOpts) 244 | copy(optsCopy[len(f.callOpts):], opts) 245 | opts = optsCopy 246 | } 247 | 248 | return newArgBuilder(opts...) 249 | } 250 | 251 | // graph adds this function to the graph. The given root should be a single 252 | // shared root to the graph, typically a rootVertex. This returns the 253 | // funcVertex created. 254 | // 255 | // includeOutput controls whether to include the output values in the graph. 256 | // This should be true for all intermediary functions but false for the 257 | // target function. 258 | func (f *Func) graph(g *graph.Graph, root graph.Vertex, includeOutput bool) graph.Vertex { 259 | vertex := g.Add(&funcVertex{ 260 | Func: f, 261 | }) 262 | 263 | // If we take no arguments, we add this function to the root 264 | // so that it isn't pruned. 265 | if f.input.empty() { 266 | g.AddEdge(vertex, root) 267 | } 268 | 269 | // Add all our inputs and add an edge from the func to the input 270 | for _, val := range f.input.values { 271 | switch val.Kind() { 272 | case ValueNamed: 273 | g.AddEdge(vertex, g.Add(&valueVertex{ 274 | Name: val.Name, 275 | Type: val.Type, 276 | Subtype: val.Subtype, 277 | })) 278 | 279 | case ValueTyped: 280 | g.AddEdgeWeighted(vertex, g.Add(&typedArgVertex{ 281 | Type: val.Type, 282 | Subtype: val.Subtype, 283 | }), weightTyped) 284 | 285 | default: 286 | panic(fmt.Sprintf("unknown value kind: %s", val.Kind())) 287 | } 288 | } 289 | 290 | if includeOutput { 291 | // Add all our outputs 292 | for k, f := range f.output.namedValues { 293 | g.AddEdge(g.Add(&valueVertex{ 294 | Name: k, 295 | Type: f.Type, 296 | Subtype: f.Subtype, 297 | }), vertex) 298 | } 299 | for _, f := range f.output.typedValues { 300 | g.AddEdgeWeighted(g.Add(&typedOutputVertex{ 301 | Type: f.Type, 302 | Subtype: f.Subtype, 303 | }), vertex, weightTyped) 304 | } 305 | } 306 | 307 | return vertex 308 | } 309 | 310 | // outputValues extracts the output from the given Result. The Result must 311 | // be a result of calling Call on this exact Func. Specifying any other 312 | // Result is undefined and will likely result in panics. 313 | func (f *Func) outputValues(r Result, vs []graph.Vertex, state *callState) { 314 | // Get our struct 315 | structVal := f.output.result(r).out[0] 316 | 317 | // Go through our out edges to find all our results so we can update 318 | // the graph nodes with our values. Along the way, we also update our 319 | // total call state. 320 | for _, v := range vs { 321 | switch v := v.(type) { 322 | case *valueVertex: 323 | // Set the value on the vertex. During the graph walk, we'll 324 | // set the Named value. 325 | v.Value = structVal.Field(f.output.namedValues[v.Name].index) 326 | 327 | case *typedOutputVertex: 328 | // Get our field with the same name 329 | field := f.output.typedValues[v.Type] 330 | v.Value = structVal.Field(field.index) 331 | } 332 | } 333 | } 334 | 335 | // errType is used for comparison in Spec 336 | var errType = reflect.TypeOf((*error)(nil)).Elem() 337 | -------------------------------------------------------------------------------- /func_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package argmapper 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "reflect" 11 | "strconv" 12 | "strings" 13 | "testing" 14 | 15 | "github.com/hashicorp/go-hclog" 16 | "github.com/stretchr/testify/require" 17 | ) 18 | 19 | func init() { 20 | hclog.L().SetLevel(hclog.Trace) 21 | } 22 | 23 | func TestFuncCall(t *testing.T) { 24 | // Used in some test cases. 25 | errSentinel := errors.New("error") 26 | 27 | // State used for some tests, reset each call 28 | var intState int 29 | 30 | cases := []struct { 31 | Name string 32 | Callback interface{} 33 | Args []Arg 34 | Out []interface{} 35 | Err string 36 | }{ 37 | { 38 | "basic named matching", 39 | func(in struct { 40 | Struct 41 | 42 | A, B int 43 | }) int { 44 | return in.A + in.B 45 | }, 46 | []Arg{ 47 | Named("a", 12), 48 | Named("b", 24), 49 | }, 50 | []interface{}{ 51 | 36, 52 | }, 53 | "", 54 | }, 55 | 56 | { 57 | "basic named matching (pointer struct)", 58 | func(in *struct { 59 | Struct 60 | 61 | A, B int 62 | }) int { 63 | return in.A + in.B 64 | }, 65 | []Arg{ 66 | Named("a", 12), 67 | Named("b", 24), 68 | }, 69 | []interface{}{ 70 | 36, 71 | }, 72 | "", 73 | }, 74 | 75 | { 76 | "basic matching with types", 77 | func(in struct { 78 | Struct 79 | 80 | A int 81 | }) int { 82 | return in.A 83 | }, 84 | []Arg{ 85 | Typed(42), 86 | }, 87 | []interface{}{ 88 | 42, 89 | }, 90 | "", 91 | }, 92 | 93 | { 94 | "basic matching with interface implementation", 95 | func(in struct { 96 | Struct 97 | 98 | A error 99 | }) (int, error) { 100 | return strconv.Atoi(in.A.Error()) 101 | }, 102 | []Arg{ 103 | Typed(fmt.Errorf("42")), 104 | }, 105 | []interface{}{ 106 | 42, 107 | }, 108 | "", 109 | }, 110 | 111 | { 112 | "basic matching with context", 113 | func(ctx context.Context) error { return ctx.Err() }, 114 | []Arg{ 115 | Typed(context.Background()), 116 | }, 117 | []interface{}{}, 118 | "", 119 | }, 120 | 121 | { 122 | "missing argument", 123 | func(in struct { 124 | Struct 125 | 126 | A, B int 127 | }) int { 128 | return in.A + in.B 129 | }, 130 | []Arg{ 131 | Named("a", 12), 132 | }, 133 | nil, 134 | "could not be satisfied", 135 | }, 136 | 137 | { 138 | "unexported field ignored", 139 | func(in struct { 140 | Struct 141 | 142 | A int 143 | b int 144 | }) int { 145 | return in.A 146 | }, 147 | []Arg{Named("a", 12)}, 148 | []interface{}{12}, 149 | "", 150 | }, 151 | 152 | { 153 | "no return values, only errors", 154 | func(in struct { 155 | Struct 156 | 157 | A, B int 158 | }) error { 159 | if in.A != 12 || in.B != 24 { 160 | // We panic to signal failure here instead of using 161 | // t but this should never happen. 162 | panic("failure") 163 | } 164 | 165 | return errSentinel 166 | }, 167 | []Arg{ 168 | Named("a", 12), 169 | Named("b", 24), 170 | }, 171 | []interface{}{}, 172 | errSentinel.Error(), 173 | }, 174 | 175 | { 176 | "renamed field", 177 | func(in struct { 178 | Struct 179 | 180 | A int `argmapper:"C"` 181 | B int 182 | }) int { 183 | return in.A + in.B 184 | }, 185 | []Arg{ 186 | Named("b", 24), 187 | Named("c", 12), 188 | }, 189 | []interface{}{ 190 | 36, 191 | }, 192 | "", 193 | }, 194 | 195 | { 196 | "typed and named prefers named", 197 | func(in struct { 198 | Struct 199 | 200 | A int 201 | }) int { 202 | return in.A 203 | }, 204 | []Arg{ 205 | Named("a", 12), 206 | Typed(42), 207 | }, 208 | []interface{}{ 209 | 12, 210 | }, 211 | "", 212 | }, 213 | 214 | { 215 | "full struct matching", 216 | func(in struct { 217 | A int 218 | B int 219 | }) int { 220 | return in.A + in.B 221 | }, 222 | []Arg{ 223 | Named("a", struct{ A, B int }{A: 12, B: 24}), 224 | }, 225 | []interface{}{ 226 | 36, 227 | }, 228 | "", 229 | }, 230 | 231 | //----------------------------------------------------------- 232 | // TYPED INPUT - STRUCT 233 | 234 | { 235 | "type only input", 236 | func(in struct { 237 | Struct 238 | 239 | A int `argmapper:",typeOnly"` 240 | }) int { 241 | return in.A 242 | }, 243 | []Arg{ 244 | Named("b", 24), 245 | }, 246 | []interface{}{ 247 | 24, 248 | }, 249 | "", 250 | }, 251 | 252 | //----------------------------------------------------------- 253 | // TYPED INPUT - NON-STRUCT 254 | 255 | { 256 | "type only input non struct", 257 | func(v int) int { return v }, 258 | []Arg{ 259 | Named("b", 24), 260 | }, 261 | []interface{}{ 262 | 24, 263 | }, 264 | "", 265 | }, 266 | 267 | //----------------------------------------------------------- 268 | // TYPE CONVERTER FUNCTIONS - NO STRUCT INPUTS 269 | 270 | { 271 | "type converter with no struct", 272 | func(in struct { 273 | Struct 274 | 275 | A string 276 | B int 277 | }) string { 278 | return strings.Repeat(in.A, in.B) 279 | }, 280 | []Arg{ 281 | Named("a", 12), 282 | Named("b", 2), 283 | Converter(func(in int) string { return strconv.Itoa(in) }), 284 | }, 285 | []interface{}{"1212"}, 286 | "", 287 | }, 288 | 289 | { 290 | "type converter both ways", 291 | func(in struct { 292 | Struct 293 | 294 | A string 295 | B int 296 | }) string { 297 | return strings.Repeat(in.A, in.B) 298 | }, 299 | []Arg{ 300 | Named("a", 12), 301 | Named("b", 2), 302 | Converter(func(in int) string { return strconv.Itoa(in) }), 303 | Converter(func(in string) (int, error) { return strconv.Atoi(in) }), 304 | }, 305 | []interface{}{"1212"}, 306 | "", 307 | }, 308 | 309 | { 310 | "type converter both ways, execute both", 311 | func(in struct { 312 | Struct 313 | 314 | A string 315 | B int 316 | }) string { 317 | return strings.Repeat(in.A, in.B) 318 | }, 319 | []Arg{ 320 | Named("a", 12), 321 | Named("b", "2"), 322 | Converter(func(in int) string { return strconv.Itoa(in) }), 323 | Converter(func(in string) (int, error) { return strconv.Atoi(in) }), 324 | }, 325 | []interface{}{"1212"}, 326 | "", 327 | }, 328 | 329 | { 330 | "type converter with an error", 331 | func(in struct { 332 | Struct 333 | 334 | A string 335 | }) string { 336 | return in.A 337 | }, 338 | []Arg{ 339 | Named("a", 12), 340 | Converter(func(in int) (string, error) { 341 | return "", fmt.Errorf("failed") 342 | }), 343 | }, 344 | nil, 345 | "failed", 346 | }, 347 | 348 | { 349 | "type converter with an error that succeeds", 350 | func(in struct { 351 | Struct 352 | 353 | A string 354 | }) string { 355 | return in.A 356 | }, 357 | []Arg{ 358 | Named("a", 12), 359 | Converter(func(in int) (string, error) { 360 | return "YAY", nil 361 | }), 362 | }, 363 | []interface{}{"YAY"}, 364 | "", 365 | }, 366 | 367 | { 368 | "multiple input converter with no struct", 369 | func(in struct { 370 | Struct 371 | 372 | A int 373 | }) string { 374 | return strconv.Itoa(in.A) 375 | }, 376 | []Arg{ 377 | Named("a", "foo"), 378 | Named("b", 2), 379 | Converter(func(v string, n int) int { return len(v) + n }), 380 | }, 381 | []interface{}{"5"}, 382 | "", 383 | }, 384 | 385 | { 386 | "chained converters", 387 | func(in struct { 388 | Struct 389 | 390 | A int 391 | }) string { 392 | return strconv.Itoa(in.A) 393 | }, 394 | []Arg{ 395 | Named("input", 12), 396 | Converter(func(in int) string { return strconv.Itoa(in) }), 397 | Converter(func(in string) int { return len(in) }), 398 | }, 399 | []interface{}{"2"}, 400 | "", 401 | }, 402 | 403 | { 404 | "chained multiple input converter", 405 | func(in struct { 406 | Struct 407 | 408 | A int 409 | B string 410 | }) string { 411 | return in.B + " " + strconv.Itoa(in.A) 412 | }, 413 | []Arg{ 414 | Typed(int8(42)), 415 | Typed(int16(1)), 416 | Converter(func(v int8) int { return int(v) }), 417 | Converter(func(v int, n int16) string { return "hello" }), 418 | }, 419 | []interface{}{"hello 42"}, 420 | "", 421 | }, 422 | 423 | { 424 | "no argument converter", 425 | func(in struct { 426 | Struct 427 | 428 | A int 429 | B string 430 | }) string { 431 | return in.B + strconv.Itoa(in.A) 432 | }, 433 | []Arg{ 434 | Named("a", 12), 435 | Converter(func() string { return "yes: " }), 436 | }, 437 | []interface{}{"yes: 12"}, 438 | "", 439 | }, 440 | 441 | { 442 | "only providers", 443 | func(in struct { 444 | Struct 445 | 446 | A int 447 | B string 448 | }) string { 449 | return in.B + strconv.Itoa(in.A) 450 | }, 451 | []Arg{ 452 | Converter(func() string { return "yes: " }), 453 | Converter(func() int { return 12 }), 454 | }, 455 | []interface{}{"yes: 12"}, 456 | "", 457 | }, 458 | 459 | { 460 | "multi-type providers", 461 | func(in struct { 462 | Struct 463 | 464 | A int 465 | B string 466 | }) string { 467 | return in.B + strconv.Itoa(in.A) 468 | }, 469 | []Arg{ 470 | Converter(func() (string, int) { return "yes: ", 12 }), 471 | }, 472 | []interface{}{"yes: 12"}, 473 | "", 474 | }, 475 | 476 | { 477 | "multi-type converter with cycle", 478 | func(a int, b string) string { 479 | return b + strconv.Itoa(a) 480 | }, 481 | []Arg{ 482 | Typed("no: "), 483 | Converter(func(int) (string, int) { return "yes: ", 12 }), 484 | }, 485 | nil, 486 | "could not be satisfied", 487 | }, 488 | 489 | { 490 | "unnecessary converters", 491 | func(ctx context.Context, v string) string { return v }, 492 | []Arg{ 493 | Typed(context.Background()), 494 | Typed(42), 495 | Converter(func(v int) string { return strconv.Itoa(v) }), 496 | Converter(func(context.Context, []byte) string { return "" }), 497 | Converter(func(context.Context, string) []byte { return []byte("") }), 498 | }, 499 | []interface{}{"42"}, 500 | "", 501 | }, 502 | 503 | { 504 | "duplicate type converters", 505 | func(in struct { 506 | Struct 507 | 508 | A string 509 | B int 510 | }) string { 511 | return strings.Repeat(in.A, in.B) 512 | }, 513 | []Arg{ 514 | Named("a", 12), 515 | Named("b", 2), 516 | Converter(func(in int) string { return strconv.Itoa(in) }), 517 | Converter(func(in int) string { return strconv.Itoa(in) }), 518 | Converter(func(in int) string { return strconv.Itoa(in) }), 519 | Converter(func(in int) string { return strconv.Itoa(in) }), 520 | }, 521 | []interface{}{"1212"}, 522 | "", 523 | }, 524 | 525 | //----------------------------------------------------------- 526 | // TYPE CONVERTER STRUCTS 527 | 528 | { 529 | "direct named converter", 530 | func(in struct { 531 | Struct 532 | 533 | A string 534 | }) string { 535 | return in.A + "!" 536 | }, 537 | []Arg{ 538 | Named("a", 12), 539 | Converter(func(s struct { 540 | Struct 541 | 542 | A int 543 | }) struct { 544 | Struct 545 | 546 | A string 547 | } { 548 | return struct { 549 | Struct 550 | 551 | A string 552 | }{A: strconv.Itoa(s.A)} 553 | }), 554 | }, 555 | []interface{}{"12!"}, 556 | "", 557 | }, 558 | 559 | { 560 | "direct named converter (pointer)", 561 | func(in struct { 562 | Struct 563 | 564 | A string 565 | }) string { 566 | return in.A + "!" 567 | }, 568 | []Arg{ 569 | Named("a", 12), 570 | Converter(func(s struct { 571 | Struct 572 | 573 | A int 574 | }) *struct { 575 | Struct 576 | 577 | A string 578 | } { 579 | return &struct { 580 | Struct 581 | 582 | A string 583 | }{A: strconv.Itoa(s.A)} 584 | }), 585 | }, 586 | []interface{}{"12!"}, 587 | "", 588 | }, 589 | 590 | { 591 | "direct named converter (pointer, nil result)", 592 | func(in struct { 593 | Struct 594 | 595 | A string 596 | }) string { 597 | return in.A + "!" 598 | }, 599 | []Arg{ 600 | Named("a", 12), 601 | Converter(func(s struct { 602 | Struct 603 | 604 | A int 605 | }) *struct { 606 | Struct 607 | 608 | A string 609 | } { 610 | return nil 611 | }), 612 | }, 613 | []interface{}{"!"}, 614 | "", 615 | }, 616 | 617 | { 618 | "generic type converter", 619 | func(in struct { 620 | Struct 621 | 622 | A string 623 | B int 624 | }) string { 625 | return strings.Repeat(in.A, in.B) 626 | }, 627 | []Arg{ 628 | Named("a", 12), 629 | Named("b", 2), 630 | Converter(func(s struct { 631 | Struct 632 | C string 633 | }) struct { 634 | Struct 635 | A string 636 | } { 637 | return struct { 638 | Struct 639 | A string 640 | }{A: "FOO"} 641 | }), 642 | Converter(func(s struct { 643 | Struct 644 | C bool 645 | }) struct { 646 | Struct 647 | A string 648 | } { 649 | return struct { 650 | Struct 651 | A string 652 | }{A: "FOO"} 653 | }), 654 | Converter(func(s struct { 655 | Struct 656 | B int `argmapper:",typeOnly"` 657 | }) struct { 658 | Struct 659 | B string `argmapper:",typeOnly"` 660 | } { 661 | return struct { 662 | Struct 663 | B string `argmapper:",typeOnly"` 664 | }{B: strconv.Itoa(s.B)} 665 | }), 666 | }, 667 | []interface{}{"1212"}, 668 | "", 669 | }, 670 | 671 | { 672 | "type converter with multiple typeOnly fields", 673 | func(in struct { 674 | Struct 675 | A string 676 | B int 677 | }) string { 678 | return strings.Repeat(in.A, in.B) 679 | }, 680 | []Arg{ 681 | Named("a", 12), 682 | Named("b", "AB"), 683 | Converter(func(s struct { 684 | Struct 685 | C string `argmapper:",typeOnly"` 686 | D int `argmapper:",typeOnly"` 687 | }) struct { 688 | Struct 689 | C int `argmapper:",typeOnly"` 690 | D string `argmapper:",typeOnly"` 691 | } { 692 | return struct { 693 | Struct 694 | C int `argmapper:",typeOnly"` 695 | D string `argmapper:",typeOnly"` 696 | }{C: len(s.C), D: strconv.Itoa(s.D)} 697 | }), 698 | }, 699 | []interface{}{"1212"}, 700 | "", 701 | }, 702 | 703 | //----------------------------------------------------------- 704 | // SUBTYPES 705 | 706 | { 707 | "subtype named match", 708 | func(in struct { 709 | Struct 710 | 711 | A int `argmapper:",subtype=foo"` 712 | }) int { 713 | return in.A 714 | }, 715 | []Arg{ 716 | NamedSubtype("a", 24, "bar"), 717 | NamedSubtype("a", 36, "foo"), 718 | }, 719 | []interface{}{ 720 | 36, 721 | }, 722 | "", 723 | }, 724 | 725 | { 726 | "subtype no match", 727 | func(in struct { 728 | Struct 729 | 730 | A int `argmapper:",subtype=foo"` 731 | }) int { 732 | return in.A 733 | }, 734 | []Arg{ 735 | NamedSubtype("a", 24, "bar"), 736 | }, 737 | nil, 738 | "could not be", 739 | }, 740 | 741 | { 742 | "subtype named conversion", 743 | func(in struct { 744 | Struct 745 | 746 | A int `argmapper:",subtype=foo"` 747 | }) int { 748 | return in.A 749 | }, 750 | []Arg{ 751 | NamedSubtype("a", 24, "bar"), 752 | Converter(func(s struct { 753 | Struct 754 | 755 | A int `argmapper:",subtype=bar"` 756 | }) struct { 757 | Struct 758 | 759 | A int `argmapper:",subtype=foo"` 760 | } { 761 | return struct { 762 | Struct 763 | 764 | A int `argmapper:",subtype=foo"` 765 | }{A: s.A} 766 | }), 767 | }, 768 | []interface{}{ 769 | 24, 770 | }, 771 | "", 772 | }, 773 | 774 | { 775 | "subtype named not specified", 776 | func(in struct { 777 | Struct 778 | 779 | A int 780 | }) int { 781 | return in.A 782 | }, 783 | []Arg{ 784 | NamedSubtype("a", 24, "bar"), 785 | }, 786 | []interface{}{ 787 | 24, 788 | }, 789 | "", 790 | }, 791 | 792 | { 793 | "subtype named not specified prefers exact match", 794 | func(in struct { 795 | Struct 796 | 797 | A int 798 | }) int { 799 | return in.A 800 | }, 801 | []Arg{ 802 | Named("a", 24), 803 | NamedSubtype("a", 36, "bar"), 804 | }, 805 | []interface{}{ 806 | 24, 807 | }, 808 | "", 809 | }, 810 | 811 | { 812 | "subtype type match", 813 | func(in struct { 814 | Struct 815 | 816 | A int `argmapper:",typeOnly,subtype=foo"` 817 | }) int { 818 | return in.A 819 | }, 820 | []Arg{ 821 | TypedSubtype(24, "bar"), 822 | TypedSubtype(36, "foo"), 823 | }, 824 | []interface{}{ 825 | 36, 826 | }, 827 | "", 828 | }, 829 | 830 | { 831 | "subtype type match named", 832 | func(in struct { 833 | Struct 834 | 835 | A int `argmapper:",typeOnly,subtype=foo"` 836 | }) int { 837 | return in.A 838 | }, 839 | []Arg{ 840 | Named("A", 24), 841 | NamedSubtype("A", 24, "foo"), 842 | }, 843 | []interface{}{ 844 | 24, 845 | }, 846 | "", 847 | }, 848 | 849 | { 850 | "subtype type no match", 851 | func(in struct { 852 | Struct 853 | 854 | A int `argmapper:",typeOnly,subtype=foo"` 855 | }) int { 856 | return in.A 857 | }, 858 | []Arg{ 859 | TypedSubtype(36, "bar"), 860 | }, 861 | nil, 862 | "could not", 863 | }, 864 | 865 | { 866 | "subtype type not specified", 867 | func(in struct { 868 | Struct 869 | 870 | A int `argmapper:",typeOnly"` 871 | }) int { 872 | return in.A 873 | }, 874 | []Arg{ 875 | TypedSubtype(36, "foo"), 876 | }, 877 | []interface{}{ 878 | 36, 879 | }, 880 | "", 881 | }, 882 | 883 | { 884 | "subtype type conversion", 885 | func(in struct { 886 | Struct 887 | 888 | A int `argmapper:",typeOnly,subtype=foo"` 889 | }) int { 890 | return in.A 891 | }, 892 | []Arg{ 893 | TypedSubtype(24, "bar"), 894 | Converter(func(s struct { 895 | Struct 896 | 897 | C int `argmapper:",typeOnly,subtype=bar"` 898 | }) struct { 899 | Struct 900 | 901 | D int `argmapper:",typeOnly,subtype=foo"` 902 | } { 903 | return struct { 904 | Struct 905 | 906 | D int `argmapper:",typeOnly,subtype=foo"` 907 | }{D: s.C + 1} 908 | }), 909 | }, 910 | []interface{}{ 911 | 25, 912 | }, 913 | "", 914 | }, 915 | 916 | { 917 | "subtype type conversion inherits subtype", 918 | func(in struct { 919 | Struct 920 | 921 | A int `argmapper:",typeOnly,subtype=foo"` 922 | }) int { 923 | return in.A 924 | }, 925 | []Arg{ 926 | TypedSubtype("value", "bar"), 927 | Converter(func(s struct { 928 | Struct 929 | 930 | C string `argmapper:",typeOnly"` 931 | }) struct { 932 | Struct 933 | 934 | D int `argmapper:",typeOnly"` 935 | } { 936 | return struct { 937 | Struct 938 | 939 | D int `argmapper:",typeOnly"` 940 | }{D: 42} 941 | }), 942 | }, 943 | []interface{}{ 944 | 42, 945 | }, 946 | "", 947 | }, 948 | 949 | { 950 | "subtype type conversion generated", 951 | func(in struct { 952 | Struct 953 | 954 | A int `argmapper:",typeOnly,subtype=foo"` 955 | }) int { 956 | return in.A 957 | }, 958 | []Arg{ 959 | TypedSubtype("foo", "bar"), 960 | ConverterGen(func(v Value) (*Func, error) { 961 | // We only want strings 962 | if v.Type != reflect.TypeOf("") { 963 | return nil, nil 964 | } 965 | 966 | // We take this value as our input. 967 | inputSet, err := NewValueSet([]Value{v}) 968 | if err != nil { 969 | return nil, err 970 | } 971 | 972 | // Generate an int with the subtype of the string value 973 | outputSet, err := NewValueSet([]Value{Value{ 974 | Name: v.Name, 975 | Type: reflect.TypeOf(int(0)), 976 | Subtype: v.Value.Interface().(string), 977 | }}) 978 | if err != nil { 979 | return nil, err 980 | } 981 | 982 | return BuildFunc(inputSet, outputSet, func(in, out *ValueSet) error { 983 | outputSet.Typed(reflect.TypeOf(int(0))).Value = 984 | reflect.ValueOf(42) 985 | return nil 986 | }) 987 | }), 988 | }, 989 | []interface{}{ 990 | 42, 991 | }, 992 | "", 993 | }, 994 | 995 | { 996 | "subtype type conversion generated multiple paths", 997 | func(in struct { 998 | Struct 999 | 1000 | A int `argmapper:",typeOnly,subtype=foo"` 1001 | B int `argmapper:",typeOnly,subtype=bar"` 1002 | }) int { 1003 | return in.A + in.B 1004 | }, 1005 | []Arg{ 1006 | TypedSubtype("foo", "12"), 1007 | TypedSubtype("bar", "24"), 1008 | ConverterGen(func(v Value) (*Func, error) { 1009 | // We only want strings 1010 | if v.Type != reflect.TypeOf("") { 1011 | return nil, nil 1012 | } 1013 | 1014 | // Convert subtype cause we're going to use it 1015 | subtypeInt, err := strconv.Atoi(v.Subtype) 1016 | if err != nil { 1017 | return nil, err 1018 | } 1019 | 1020 | // We take this value as our input. 1021 | inputSet, err := NewValueSet([]Value{v}) 1022 | if err != nil { 1023 | return nil, err 1024 | } 1025 | 1026 | // Generate an int with the subtype of the string value 1027 | outputSet, err := NewValueSet([]Value{Value{ 1028 | Name: v.Name, 1029 | Type: reflect.TypeOf(int(0)), 1030 | Subtype: v.Value.Interface().(string), 1031 | }}) 1032 | if err != nil { 1033 | return nil, err 1034 | } 1035 | 1036 | return BuildFunc(inputSet, outputSet, func(in, out *ValueSet) error { 1037 | outputSet.Typed(reflect.TypeOf(int(0))).Value = 1038 | reflect.ValueOf(subtypeInt) 1039 | return nil 1040 | }) 1041 | }), 1042 | }, 1043 | []interface{}{ 1044 | 36, 1045 | }, 1046 | "", 1047 | }, 1048 | 1049 | { 1050 | "subtype type matching named", 1051 | func(in struct { 1052 | Struct 1053 | 1054 | A int `argmapper:",typeOnly"` 1055 | }) int { 1056 | return in.A 1057 | }, 1058 | []Arg{ 1059 | NamedSubtype("b", 36, "bar"), 1060 | }, 1061 | []interface{}{ 1062 | 36, 1063 | }, 1064 | "", 1065 | }, 1066 | 1067 | { 1068 | "empty struct arg", 1069 | func(in struct { 1070 | Struct 1071 | 1072 | A int `argmapper:",typeOnly,subtype=A"` 1073 | B int `argmapper:",typeOnly,subtype=B"` 1074 | }) error { 1075 | return nil 1076 | }, 1077 | []Arg{ 1078 | // This empty struct arg was triggering a bug where this 1079 | // wasn't resolving properly so this test exercises that in 1080 | // an overly complex way. 1081 | Converter(func(struct { 1082 | Struct 1083 | }) (struct { 1084 | Struct 1085 | 1086 | X int `argmapper:",typeOnly,subType=A"` 1087 | }, error) { 1088 | return struct { 1089 | Struct 1090 | 1091 | X int `argmapper:",typeOnly,subType=A"` 1092 | }{X: 42}, nil 1093 | }), 1094 | 1095 | Converter(func(struct { 1096 | Struct 1097 | 1098 | X int `argmapper:",typeOnly,subType=A"` 1099 | }) (struct { 1100 | Struct 1101 | 1102 | Y int `argmapper:",typeOnly,subType=B"` 1103 | }, error) { 1104 | return struct { 1105 | Struct 1106 | 1107 | Y int `argmapper:",typeOnly,subType=B"` 1108 | }{Y: 84}, nil 1109 | }), 1110 | }, 1111 | nil, 1112 | "", 1113 | }, 1114 | 1115 | //---------------------------------------------------------------- 1116 | // Once 1117 | 1118 | { 1119 | "once option", 1120 | func(in struct { 1121 | Struct 1122 | 1123 | A string 1124 | B bool 1125 | State *int 1126 | }) string { 1127 | if !in.B { 1128 | panic("OH NO") 1129 | } 1130 | 1131 | return strconv.Itoa(*in.State) 1132 | }, 1133 | []Arg{ 1134 | Typed(&intState), 1135 | Named("b", 2), 1136 | 1137 | ConverterFunc(MustFunc(NewFunc(func(b int, state *int) struct { 1138 | Struct 1139 | 1140 | A int `argmapper:"A"` 1141 | } { 1142 | *state++ 1143 | 1144 | return struct { 1145 | Struct 1146 | A int `argmapper:"A"` 1147 | }{A: b * 2} 1148 | }, FuncOnce()))), 1149 | 1150 | Converter(func(in struct { 1151 | Struct 1152 | 1153 | A int `argmapper:"A"` 1154 | }) string { 1155 | return strconv.Itoa(in.A) 1156 | }), 1157 | 1158 | Converter(func(in struct { 1159 | Struct 1160 | 1161 | A int `argmapper:"A"` 1162 | }) bool { 1163 | return true 1164 | }), 1165 | }, 1166 | []interface{}{"1"}, 1167 | "", 1168 | }, 1169 | } 1170 | 1171 | for _, tt := range cases { 1172 | t.Run(tt.Name, func(t *testing.T) { 1173 | require := require.New(t) 1174 | 1175 | f, err := NewFunc(tt.Callback) 1176 | require.NoError(err) 1177 | result := f.Call(tt.Args...) 1178 | 1179 | // If we expect an error, check that 1180 | if tt.Err == "" { 1181 | require.NoError(result.Err()) 1182 | } else { 1183 | require.Error(result.Err()) 1184 | t.Logf("err: %s", result.Err().Error()) 1185 | require.Contains(result.Err().Error(), tt.Err) 1186 | } 1187 | 1188 | // Verify outputs 1189 | require.Equal(len(tt.Out), result.Len()) 1190 | for i, out := range tt.Out { 1191 | require.Equal(out, result.Out(i)) 1192 | } 1193 | }) 1194 | } 1195 | } 1196 | 1197 | // This tests a regression we found with argmapper after merging PR #9. 1198 | // This fails with PR #9 merged and passes without it. We added this test 1199 | // so that can revert PR #9 and safely merge a new fix when this also passes. 1200 | func TestFuncCall_waypointRepro(t *testing.T) { 1201 | type aIn struct { 1202 | Struct 1203 | N int `argmapper:",typeOnly"` 1204 | C struct{} `argmapper:",typeOnly,subtype=C"` 1205 | B struct{} `argmapper:",typeOnly,subtype=B"` 1206 | } 1207 | type aOut struct { 1208 | Struct 1209 | A struct{} `argmapper:",typeOnly,subtype=A"` 1210 | } 1211 | 1212 | type bIn struct { 1213 | Struct 1214 | N int `argmapper:",typeOnly"` 1215 | C struct{} `argmapper:",typeOnly,subtype=C"` 1216 | } 1217 | type bOut struct { 1218 | Struct 1219 | B struct{} `argmapper:",typeOnly,subtype=B"` 1220 | } 1221 | 1222 | type cIn struct { 1223 | Struct 1224 | N int `argmapper:",typeOnly"` 1225 | } 1226 | type cOut struct { 1227 | Struct 1228 | C struct{} `argmapper:",typeOnly,subtype=C"` 1229 | } 1230 | 1231 | type targetIn struct { 1232 | Struct 1233 | 1234 | // Note: this is the crux of the test: this ordering matters. 1235 | C struct{} `argmapper:",typeOnly,subtype=C"` 1236 | A struct{} `argmapper:",typeOnly,subtype=A"` 1237 | B struct{} `argmapper:",typeOnly,subtype=B"` 1238 | } 1239 | 1240 | a := MustFunc(NewFunc(func(aIn) (aOut, error) { 1241 | return aOut{A: struct{}{}}, nil 1242 | })) 1243 | b := MustFunc(NewFunc(func(bIn) (bOut, error) { 1244 | return bOut{B: struct{}{}}, nil 1245 | })) 1246 | c := MustFunc(NewFunc(func(cIn) (cOut, error) { 1247 | return cOut{C: struct{}{}}, nil 1248 | })) 1249 | 1250 | target := MustFunc(NewFunc(func(targetIn) error { 1251 | return nil 1252 | })) 1253 | 1254 | for i := 0; i < 100; i++ { 1255 | result := target.Call( 1256 | Typed(int(42)), 1257 | ConverterFunc(c, a, b), 1258 | ) 1259 | require.NoError(t, result.Err()) 1260 | } 1261 | } 1262 | 1263 | func TestBuildFunc(t *testing.T) { 1264 | require := require.New(t) 1265 | 1266 | intType := reflect.TypeOf(int(0)) 1267 | 1268 | input, err := NewValueSet([]Value{ 1269 | Value{ 1270 | Name: "a", 1271 | Type: intType, 1272 | }, 1273 | }) 1274 | require.NoError(err) 1275 | 1276 | output, err := NewValueSet([]Value{ 1277 | Value{ 1278 | Type: intType, 1279 | }, 1280 | }) 1281 | require.NoError(err) 1282 | 1283 | f, err := BuildFunc(input, output, func(in, out *ValueSet) error { 1284 | // Double 1285 | result := in.Named("a").Value.Interface().(int) * 2 1286 | 1287 | // Set the result 1288 | out.Typed(intType).Value = reflect.ValueOf(result) 1289 | 1290 | return nil 1291 | }) 1292 | require.NoError(err) 1293 | 1294 | require.NoError(output.FromResult(f.Call(Named("a", 12)))) 1295 | require.Equal(24, output.Typed(intType).Value.Interface()) 1296 | } 1297 | 1298 | // This tests a cycle found while working on the Waypoint project. 1299 | // We were using argmapper in a broken way so this should've never 1300 | // worked but it caused an infinite loop instead. This test originally 1301 | // infinite looped and remains to verify we don't regress. 1302 | func TestBuildFunc_waypointCycle(t *testing.T) { 1303 | // This copies our behavior from Waypoint that triggered this bug. 1304 | type markerType struct{} 1305 | markerValue := func(n string) Value { 1306 | val := markerType(struct{}{}) 1307 | return Value{ 1308 | Type: reflect.TypeOf(val), 1309 | Subtype: n, 1310 | Value: reflect.ValueOf(val), 1311 | } 1312 | } 1313 | 1314 | require := require.New(t) 1315 | 1316 | intType := reflect.TypeOf(int(0)) 1317 | stringType := reflect.TypeOf("") 1318 | 1319 | // Input is (int, string) 1320 | input, err := NewValueSet([]Value{ 1321 | { 1322 | Type: intType, 1323 | }, 1324 | { 1325 | Type: stringType, 1326 | }, 1327 | }) 1328 | require.NoError(err) 1329 | 1330 | // Output is (marker<"A">, int) 1331 | markerVal := markerValue("A") 1332 | output, err := NewValueSet([]Value{ 1333 | markerVal, 1334 | { 1335 | Type: intType, 1336 | }, 1337 | }) 1338 | require.NoError(err) 1339 | 1340 | // Our function does nothing 1341 | f, err := BuildFunc(input, output, func(in, out *ValueSet) error { 1342 | return nil 1343 | }) 1344 | require.NoError(err) 1345 | 1346 | // Our target function just wants the marker value. 1347 | input, err = NewValueSet([]Value{markerVal}) 1348 | require.NoError(err) 1349 | target, err := BuildFunc(input, nil, func(in, out *ValueSet) error { 1350 | return nil 1351 | }) 1352 | require.NoError(err) 1353 | 1354 | // Input is the string and the converter. 1355 | result := target.Call( 1356 | Typed("hello"), 1357 | ConverterFunc(f), 1358 | ) 1359 | require.Error(result.Err()) 1360 | } 1361 | 1362 | func TestBuildFunc_noOutput(t *testing.T) { 1363 | require := require.New(t) 1364 | 1365 | f, err := NewFunc(func(a int) {}) 1366 | require.NoError(err) 1367 | 1368 | f, err = BuildFunc(f.Input(), f.Output(), func(in, out *ValueSet) error { 1369 | return nil 1370 | }) 1371 | require.NoError(err) 1372 | require.NoError(f.Output().FromResult(f.Call(Named("a", 12)))) 1373 | } 1374 | 1375 | func TestBuildFunc_nilOutput(t *testing.T) { 1376 | require := require.New(t) 1377 | 1378 | f, err := NewFunc(func(a int) {}) 1379 | require.NoError(err) 1380 | 1381 | f, err = BuildFunc(f.Input(), nil, func(in, out *ValueSet) error { 1382 | return nil 1383 | }) 1384 | require.NoError(err) 1385 | require.NoError(f.Output().FromResult(f.Call(Named("a", 12)))) 1386 | } 1387 | 1388 | func TestBuildFunc_nilInput(t *testing.T) { 1389 | require := require.New(t) 1390 | 1391 | f, err := NewFunc(func(a *int) {}) 1392 | require.NoError(err) 1393 | 1394 | // Should not panic with a nil arg 1395 | require.Error(f.Output().FromResult(f.Call(nil))) 1396 | } 1397 | 1398 | func TestBuildFunc_errValue(t *testing.T) { 1399 | require := require.New(t) 1400 | 1401 | intType := reflect.TypeOf(int(0)) 1402 | 1403 | f, err := NewFunc(func(a int) {}) 1404 | require.NoError(err) 1405 | 1406 | input, err := NewValueSet([]Value{ 1407 | Value{ 1408 | Name: "a", 1409 | Type: intType, 1410 | }, 1411 | }) 1412 | require.NoError(err) 1413 | 1414 | f, err = BuildFunc(input, f.Output(), func(in, out *ValueSet) error { 1415 | return fmt.Errorf("some error") 1416 | }) 1417 | require.NoError(err) 1418 | 1419 | require.EqualError(f.Output().FromResult(f.Call(Named("a", 12))), "some error") 1420 | } 1421 | 1422 | func TestFunc_defaultOpts(t *testing.T) { 1423 | f, err := NewFunc(func(v int) string { 1424 | return strconv.Itoa(v) 1425 | }, Named("a", 42)) 1426 | require.NoError(t, err) 1427 | 1428 | result := f.Call(Typed("foo")) 1429 | require.NoError(t, result.Err()) 1430 | require.Equal(t, "42", result.Out(0)) 1431 | } 1432 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/go-argmapper 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/hashicorp/go-hclog v1.6.3 7 | github.com/hashicorp/go-multierror v1.1.1 8 | github.com/stretchr/testify v1.10.0 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/fatih/color v1.13.0 // indirect 14 | github.com/hashicorp/errwrap v1.0.0 // indirect 15 | github.com/kr/pretty v0.1.0 // indirect 16 | github.com/mattn/go-colorable v0.1.12 // indirect 17 | github.com/mattn/go-isatty v0.0.14 // indirect 18 | github.com/pmezard/go-difflib v1.0.0 // indirect 19 | golang.org/x/sys v0.1.0 // indirect 20 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 21 | gopkg.in/yaml.v3 v3.0.1 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 5 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 6 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 7 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 8 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= 9 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 10 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 11 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 12 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 13 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 14 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 15 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 16 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 17 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 18 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 19 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 20 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 21 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 22 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 23 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 24 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 25 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 26 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 27 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 28 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 29 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 30 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 31 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 33 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 34 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= 35 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 36 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 37 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 38 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 39 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 40 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 41 | -------------------------------------------------------------------------------- /graph.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package argmapper 5 | 6 | import ( 7 | "fmt" 8 | "reflect" 9 | 10 | "github.com/hashicorp/go-argmapper/internal/graph" 11 | ) 12 | 13 | const ( 14 | // weightNormal is the typcal edge weight. 15 | //nolint:unused 16 | weightNormal = 1 17 | 18 | // weightTyped is the weight to use for edges that connected to any 19 | // type-only vertex. We weigh these heavier since we prefer valued vertices. 20 | weightTyped = 5 21 | 22 | // weightTypedOtherSubtype is the weight to use for edges that connect 23 | // types that match but subtypes that do not match. 24 | weightTypedOtherSubtype = 20 25 | 26 | // weightMatchingName is the weight to use for the edges to any value 27 | // vertex with a matching name. This has the effect of preferring edges 28 | // from "A string" to "A int" for example (over "B string" to "A int"), 29 | // since we'd prefer to convert our original type. 30 | weightMatchingName = -1 31 | ) 32 | 33 | // valueConverter is the interface implemented by vertices that can 34 | // be represented by values. This is used to convert unexported vertex 35 | // implementations into user-friendly information about what they represent. 36 | type valueConverter interface { 37 | value() *Value 38 | } 39 | 40 | // valueVertex represents any named and typed value. 41 | type valueVertex struct { 42 | Name string 43 | Type reflect.Type 44 | Subtype string 45 | Value reflect.Value 46 | } 47 | 48 | func (v *valueVertex) Hashcode() interface{} { 49 | return fmt.Sprintf("%s/%s/%s", v.Name, v.Type.String(), v.Subtype) 50 | } 51 | 52 | // value returns the Value structures for this vertex. This is useful 53 | // for error messages and other points where we must convert this to an 54 | // exported, user-usable value. 55 | func (v *valueVertex) value() *Value { 56 | return &Value{ 57 | Name: v.Name, 58 | Type: v.Type, 59 | Subtype: v.Subtype, 60 | Value: v.Value, 61 | } 62 | } 63 | 64 | // funcVertex is our target function. There is only ever one of these 65 | // in the graph. 66 | type funcVertex struct { 67 | Func *Func 68 | } 69 | 70 | func (v *funcVertex) Hashcode() interface{} { return v.Func.fn.Type() } 71 | func (v *funcVertex) String() string { return "func: " + v.Func.fn.String() } 72 | 73 | // typedArgVertex represents a typed argument to a function. These have no 74 | // name and match any matching types. 75 | type typedArgVertex struct { 76 | Name string 77 | Type reflect.Type 78 | Subtype string 79 | 80 | Value reflect.Value 81 | } 82 | 83 | func (v *typedArgVertex) Hashcode() interface{} { 84 | return fmt.Sprintf("arg: %s/%s", v.Type.String(), v.Subtype) 85 | } 86 | 87 | func (v *typedArgVertex) String() string { return v.Hashcode().(string) } 88 | 89 | // See valueVertex.value 90 | func (v *typedArgVertex) value() *Value { 91 | return &Value{ 92 | Name: v.Name, 93 | Type: v.Type, 94 | Subtype: v.Subtype, 95 | Value: v.Value, 96 | } 97 | } 98 | 99 | // typedOutputVertex represents an output from a function that is typed 100 | // only and has no name. This can be inherited by any value with a matching 101 | // type. 102 | type typedOutputVertex struct { 103 | Name string 104 | Type reflect.Type 105 | Subtype string 106 | 107 | Value reflect.Value 108 | } 109 | 110 | func (v *typedOutputVertex) Hashcode() interface{} { 111 | return fmt.Sprintf("out: %s/%s", v.Type.String(), v.Subtype) 112 | } 113 | 114 | func (v *typedOutputVertex) String() string { 115 | str := v.Hashcode().(string) 116 | if v.Value.IsValid() { 117 | str += fmt.Sprintf(" (value: %v)", v.Value.Interface()) 118 | } 119 | 120 | return str 121 | } 122 | 123 | // See valueVertex.value 124 | func (v *typedOutputVertex) value() *Value { 125 | return &Value{ 126 | Name: v.Name, 127 | Type: v.Type, 128 | Subtype: v.Subtype, 129 | Value: v.Value, 130 | } 131 | } 132 | 133 | // rootVertex tracks the root of a function call. This should have 134 | // in-edges only from the inputs. We use this to get a single root. 135 | type rootVertex struct{} 136 | 137 | func (v *rootVertex) String() string { return "root" } 138 | 139 | var ( 140 | _ graph.VertexHashable = (*funcVertex)(nil) 141 | _ graph.VertexHashable = (*valueVertex)(nil) 142 | _ graph.VertexHashable = (*typedArgVertex)(nil) 143 | _ graph.VertexHashable = (*typedOutputVertex)(nil) 144 | ) 145 | -------------------------------------------------------------------------------- /internal/graph/dfs.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package graph 5 | 6 | type DFSFunc func(Vertex, func() error) error 7 | 8 | func (g *Graph) DFS(start Vertex, cb DFSFunc) error { 9 | return g.dfs(cb, map[interface{}]struct{}{}, hashcode(start)) 10 | } 11 | 12 | func (g *Graph) dfs(cb DFSFunc, visited map[interface{}]struct{}, v interface{}) error { 13 | /* 14 | procedure DFS(G, v) is 15 | label v as discovered 16 | for all directed edges from v to w that are in G.adjacentEdges(v) do 17 | if vertex w is not labeled as discovered then 18 | recursively call DFS(G, w) 19 | */ 20 | 21 | // Make our map for visited 22 | visited[v] = struct{}{} 23 | 24 | // for all directed edges from v to w that are in G.adjacentEdges(v) do 25 | for w := range g.adjacencyOut[v] { 26 | // if vertex w is not labeled as discovered then 27 | if _, ok := visited[w]; !ok { 28 | // call our callback 29 | if err := cb(g.hash[w], func() error { 30 | // recursively call DFS(G, w) 31 | return g.dfs(cb, visited, w) 32 | }); err != nil { 33 | return err 34 | } 35 | } 36 | } 37 | 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /internal/graph/dijkstra.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package graph 5 | 6 | import ( 7 | "container/heap" 8 | "math" 9 | ) 10 | 11 | // Dijkstra implements Dijkstra's algorithm for finding single source 12 | // shortest paths in an edge-weighted graph with non-negative edge weights. 13 | // The graph may have cycles. 14 | func (g *Graph) Dijkstra(src Vertex) (distTo map[interface{}]int, edgeTo map[interface{}]Vertex) { 15 | srchash := hashcode(src) 16 | 17 | /* 18 | for each vertex V in G 19 | distance[V] <- infinite 20 | previous[V] <- NULL 21 | */ 22 | queue := make(distQueue, 0, len(g.hash)) 23 | queueItem := map[interface{}]*distQueueItem{} 24 | for k := range g.hash { 25 | item := &distQueueItem{ 26 | v: k, 27 | distance: math.MaxInt32, 28 | previous: nil, 29 | index: len(queue), 30 | } 31 | queueItem[k] = item 32 | 33 | // If V != S, add V to Priority Queue Q 34 | queue = append(queue, item) 35 | } 36 | 37 | // distance[S] <- 0 38 | queueItem[srchash].distance = 0 39 | 40 | // Init the heap so we can use the queue 41 | heap.Init(&queue) 42 | 43 | // while Q IS NOT EMPTY 44 | visited := map[interface{}]struct{}{} 45 | for queue.Len() > 0 { 46 | // U <- Extract MIN from Q 47 | u := heap.Pop(&queue).(*distQueueItem) 48 | visited[u.v] = struct{}{} 49 | 50 | // for each unvisited neighbour V of U 51 | for vhash, weight := range g.adjacencyOut[u.v] { 52 | if _, ok := visited[vhash]; ok { 53 | continue 54 | } 55 | 56 | v := queueItem[vhash] 57 | 58 | // tempDistance <- distance[U] + edge_weight(U, V) 59 | tempDistance := u.distance + int32(weight) 60 | 61 | // if tempDistance < distance[V] 62 | if tempDistance < v.distance { 63 | // distance[V] <- tempDistance 64 | // previous[V] <- U 65 | v.distance = tempDistance 66 | v.previous = u.v 67 | heap.Fix(&queue, v.index) 68 | } 69 | } 70 | } 71 | 72 | // Return our distance and previous map 73 | distTo = make(map[interface{}]int, len(queueItem)) 74 | edgeTo = make(map[interface{}]Vertex, len(queueItem)) 75 | for _, item := range queueItem { 76 | distTo[item.v] = int(item.distance) 77 | edgeTo[item.v] = g.hash[item.previous] 78 | } 79 | 80 | return distTo, edgeTo 81 | } 82 | 83 | // distQueue is a priority queue implementation on top of a heap that 84 | // is used by Dijkstra to keep track of state. heap.Pop on this queue 85 | // will return the item with the minimal "distance" value. 86 | type distQueue []*distQueueItem 87 | 88 | type distQueueItem struct { 89 | v interface{} // Vertex hashcode 90 | distance int32 91 | previous interface{} // Previous vertex hashcode 92 | index int 93 | } 94 | 95 | func (pq distQueue) Len() int { return len(pq) } 96 | 97 | func (pq distQueue) Less(i, j int) bool { 98 | return pq[i].distance < pq[j].distance 99 | } 100 | 101 | func (pq distQueue) Swap(i, j int) { 102 | pq[i], pq[j] = pq[j], pq[i] 103 | pq[i].index = i 104 | pq[j].index = j 105 | } 106 | 107 | func (pq *distQueue) Push(x interface{}) { 108 | n := len(*pq) 109 | item := x.(*distQueueItem) 110 | item.index = n 111 | *pq = append(*pq, item) 112 | } 113 | 114 | func (pq *distQueue) Pop() interface{} { 115 | old := *pq 116 | n := len(old) 117 | item := old[n-1] 118 | old[n-1] = nil // avoid memory leak 119 | item.index = -1 // for safety 120 | *pq = old[0 : n-1] 121 | return item 122 | } 123 | 124 | var _ heap.Interface = (*distQueue)(nil) 125 | -------------------------------------------------------------------------------- /internal/graph/dijkstra_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package graph 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestDijkstra(t *testing.T) { 13 | t.Run("typical", func(t *testing.T) { 14 | var g Graph 15 | g.Add("B") 16 | g.Add("C") 17 | g.Add("D") 18 | g.Add("E") 19 | g.Add("S") 20 | g.Add("T") 21 | g.AddEdgeWeighted("S", "B", 4) 22 | g.AddEdgeWeighted("S", "C", 2) 23 | g.AddEdgeWeighted("B", "C", 1) 24 | g.AddEdgeWeighted("B", "D", 5) 25 | g.AddEdgeWeighted("C", "D", 8) 26 | g.AddEdgeWeighted("C", "E", 10) 27 | g.AddEdgeWeighted("D", "E", 2) 28 | g.AddEdgeWeighted("D", "T", 6) 29 | g.AddEdgeWeighted("E", "T", 2) 30 | 31 | distTo, edgeTo := g.Dijkstra("S") 32 | 33 | path := []interface{}{"T"} 34 | for next := edgeTo[path[0]]; next != nil; next = edgeTo[next] { 35 | path = append(path, next) 36 | } 37 | 38 | require.Equal(t, 13, distTo["T"]) 39 | require.Equal(t, []interface{}{"T", "E", "D", "B", "S"}, path) 40 | }) 41 | 42 | t.Run("cycle", func(t *testing.T) { 43 | var g Graph 44 | g.Add("A") 45 | g.Add("B") 46 | g.Add("C") 47 | g.Add("D") 48 | g.Add("E") 49 | g.Add("F") 50 | g.AddEdgeWeighted("A", "B", 10) 51 | g.AddEdgeWeighted("B", "C", 1) 52 | g.AddEdgeWeighted("C", "E", 3) 53 | g.AddEdgeWeighted("E", "D", 10) 54 | g.AddEdgeWeighted("D", "B", 10) 55 | g.AddEdgeWeighted("E", "F", 22) 56 | 57 | distTo, edgeTo := g.Dijkstra("A") 58 | 59 | path := []interface{}{"F"} 60 | for next := edgeTo[path[0]]; next != nil; next = edgeTo[next] { 61 | path = append(path, next) 62 | } 63 | 64 | require.Equal(t, 36, distTo["F"]) 65 | require.Equal(t, []interface{}{"F", "E", "C", "B", "A"}, path) 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /internal/graph/graph.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package graph 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "sort" 10 | ) 11 | 12 | // Graph represents a graph structure. 13 | // 14 | // Unless otherwise documented, it is unsafe to call any method on Graph concurrently. 15 | type Graph struct { 16 | // adjacency represents graphs using an adjaency list. Vertices are 17 | // represented using their hash codes for simpler equaliy checks. 18 | adjacencyOut map[interface{}]map[interface{}]int 19 | adjacencyIn map[interface{}]map[interface{}]int 20 | 21 | // hash maintains the mapping of hash codes to the representative Vertex. 22 | // It is assumed that two identical hashcodes of v1 and v2 are semantically 23 | // the same Vertex even if v1 != v2 in Go. 24 | hash map[interface{}]Vertex 25 | } 26 | 27 | // Add adds a vertex to the graph. If a vertex with the same identity exists 28 | // this will overwrite that vertex. 29 | func (g *Graph) Add(v Vertex) Vertex { 30 | g.init() 31 | h := hashcode(v) 32 | if _, ok := g.adjacencyOut[h]; !ok { 33 | g.adjacencyOut[h] = make(map[interface{}]int) 34 | g.adjacencyIn[h] = make(map[interface{}]int) 35 | g.hash[h] = v 36 | } 37 | return v 38 | } 39 | 40 | // AddOverwrite is the same as Add, except that even if the vertex 41 | // already exists with the same hashcode, the pointed to value is replaced 42 | // with the given v. This allows two Vertex values with the same hashcode 43 | // but different values to be replaced. 44 | func (g *Graph) AddOverwrite(v Vertex) Vertex { 45 | g.init() 46 | h := hashcode(v) 47 | g.hash[h] = v 48 | if _, ok := g.adjacencyOut[h]; !ok { 49 | g.adjacencyOut[h] = make(map[interface{}]int) 50 | g.adjacencyIn[h] = make(map[interface{}]int) 51 | } 52 | return v 53 | } 54 | 55 | // Remove removes the given vertex from the graph. 56 | func (g *Graph) Remove(v Vertex) Vertex { 57 | // Note we don't need to call init here because delete() operations 58 | // are all safe on nil maps. 59 | h := hashcode(v) 60 | 61 | // First, delete all our out-edges by deleting both the 62 | // main key as well as any of the other nodes that are tracking the 63 | // in edge 64 | for out := range g.adjacencyOut[h] { 65 | delete(g.adjacencyIn[out], h) 66 | } 67 | delete(g.adjacencyOut, h) 68 | 69 | // Same as above but for in edges 70 | for in := range g.adjacencyIn[h] { 71 | delete(g.adjacencyOut[in], h) 72 | } 73 | delete(g.adjacencyIn, h) 74 | 75 | // Forget this node completely 76 | delete(g.hash, h) 77 | return v 78 | } 79 | 80 | // Vertex returns the vertex by id. This can be done to get the node that 81 | // is actually in the graph. This will return nil if the given vertex 82 | // is not in the graph any longer. 83 | func (g *Graph) Vertex(id interface{}) Vertex { 84 | g.init() 85 | return g.hash[id] 86 | } 87 | 88 | // Vertices returns the list of all the vertices in this graph. 89 | func (g *Graph) Vertices() []Vertex { 90 | result := make([]Vertex, 0, len(g.hash)) 91 | for _, v := range g.hash { 92 | result = append(result, v) 93 | } 94 | 95 | return result 96 | } 97 | 98 | // AddEdge adds a directed edge to the graph from v1 to v2. Both v1 and v2 99 | // must already be in the Graph via Add or this will do nothing. 100 | func (g *Graph) AddEdge(v1, v2 Vertex) { 101 | g.AddEdgeWeighted(v1, v2, 1) 102 | } 103 | 104 | // AddEdgeWeighted adds a weighted edge. This is the same as AddEdge but 105 | // with the specified weight. This will overwrite any existing edges. 106 | func (g *Graph) AddEdgeWeighted(v1, v2 Vertex, weight int) { 107 | g.init() 108 | h1, h2 := hashcode(v1), hashcode(v2) 109 | g.adjacencyOut[h1][h2] = weight 110 | g.adjacencyIn[h2][h1] = weight 111 | } 112 | 113 | func (g *Graph) RemoveEdge(v1, v2 Vertex) { 114 | g.init() 115 | h1, h2 := hashcode(v1), hashcode(v2) 116 | delete(g.adjacencyOut[h1], h2) 117 | delete(g.adjacencyIn[h2], h1) 118 | } 119 | 120 | func (g *Graph) OutEdges(v Vertex) []Vertex { 121 | edges := g.adjacencyOut[hashcode(v)] 122 | if len(edges) == 0 { 123 | return nil 124 | } 125 | 126 | result := make([]Vertex, 0, len(edges)) 127 | for h := range edges { 128 | result = append(result, g.hash[h]) 129 | } 130 | 131 | return result 132 | } 133 | 134 | func (g *Graph) InEdges(v Vertex) []Vertex { 135 | edges := g.adjacencyIn[hashcode(v)] 136 | if len(edges) == 0 { 137 | return nil 138 | } 139 | 140 | result := make([]Vertex, 0, len(edges)) 141 | for h := range edges { 142 | result = append(result, g.hash[h]) 143 | } 144 | 145 | return result 146 | } 147 | 148 | // Reverse reverses the graph but _does not make a copy_. Any changes to 149 | // this graph will impact the original Graph. You must call Copy on the 150 | // result if you want to have a copy. 151 | func (g *Graph) Reverse() *Graph { 152 | return &Graph{ 153 | adjacencyOut: g.adjacencyIn, 154 | adjacencyIn: g.adjacencyOut, 155 | hash: g.hash, 156 | } 157 | } 158 | 159 | // Copy copies the graph. In the copy, any added or removed edges do not 160 | // affect the original graph. The vertices themselves are not deep copied. 161 | func (g *Graph) Copy() *Graph { 162 | var g2 Graph 163 | g2.init() 164 | 165 | for k, set := range g.adjacencyOut { 166 | copy := make(map[interface{}]int) 167 | for k, v := range set { 168 | copy[k] = v 169 | } 170 | g2.adjacencyOut[k] = copy 171 | } 172 | for k, set := range g.adjacencyIn { 173 | copy := make(map[interface{}]int) 174 | for k, v := range set { 175 | copy[k] = v 176 | } 177 | g2.adjacencyIn[k] = copy 178 | } 179 | for k, v := range g.hash { 180 | g2.hash[k] = v 181 | } 182 | 183 | return &g2 184 | } 185 | 186 | // String outputs some human-friendly output for the graph structure. 187 | func (g *Graph) String() string { 188 | var buf bytes.Buffer 189 | buf.WriteString("\n") 190 | 191 | // Build the list of node names and a mapping so that we can more 192 | // easily alphabetize the output to remain deterministic. 193 | names := make([]string, 0, len(g.hash)) 194 | mapping := make(map[string]Vertex, len(g.hash)) 195 | for _, v := range g.hash { 196 | name := VertexName(v) 197 | names = append(names, name) 198 | mapping[name] = v 199 | } 200 | sort.Strings(names) 201 | 202 | // Write each node in order... 203 | for _, name := range names { 204 | v := mapping[name] 205 | targets := g.adjacencyOut[hashcode(v)] 206 | 207 | buf.WriteString(fmt.Sprintf("%s\n", name)) 208 | 209 | // Alphabetize dependencies 210 | deps := make([]string, 0, len(targets)) 211 | for targetHash, weight := range targets { 212 | deps = append(deps, fmt.Sprintf( 213 | "%s (%d)", VertexName(g.hash[targetHash]), weight)) 214 | } 215 | sort.Strings(deps) 216 | 217 | // Write dependencies 218 | for _, d := range deps { 219 | buf.WriteString(fmt.Sprintf(" %s\n", d)) 220 | } 221 | } 222 | 223 | return buf.String() 224 | } 225 | 226 | func (g *Graph) init() { 227 | if g.adjacencyOut == nil { 228 | g.adjacencyOut = make(map[interface{}]map[interface{}]int) 229 | } 230 | if g.adjacencyIn == nil { 231 | g.adjacencyIn = make(map[interface{}]map[interface{}]int) 232 | } 233 | if g.hash == nil { 234 | g.hash = make(map[interface{}]Vertex) 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /internal/graph/kahn.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package graph 5 | 6 | import "fmt" 7 | 8 | // KahnSort will return the topological sort of the graph using Kahn's algorithm. 9 | // The graph must not have any cycles or this will panic. 10 | func (g *Graph) KahnSort() TopoOrder { 11 | /* 12 | L ← Empty list that will contain the sorted elements 13 | S ← Set of all nodes with no incoming edge 14 | 15 | while S is non-empty do 16 | remove a node n from S 17 | add n to tail of L 18 | for each node m with an edge e from n to m do 19 | remove edge e from the graph 20 | if m has no other incoming edges then 21 | insert m into S 22 | 23 | if graph has edges then 24 | return error (graph has at least one cycle) 25 | else 26 | return L (a topologically sorted order) 27 | */ 28 | 29 | // Copy the graph 30 | g = g.Copy() 31 | 32 | // L ← Empty list that will contain the sorted elements 33 | L := make([]Vertex, 0, len(g.adjacencyOut)) 34 | 35 | // S ← Set of all nodes with no incoming edge 36 | S := []interface{}{} 37 | for v, list := range g.adjacencyIn { 38 | if len(list) == 0 { 39 | S = append(S, v) 40 | } 41 | } 42 | 43 | // while S is non-empty do 44 | for len(S) > 0 { 45 | // remove a node n from S 46 | n := S[len(S)-1] 47 | S = S[:len(S)-1] 48 | 49 | // add n to tail of L 50 | L = append(L, g.hash[n]) 51 | 52 | // for each node m with an edge e from n to m do 53 | for m := range g.adjacencyOut[n] { 54 | // remove edge e from the graph 55 | g.RemoveEdge(n, m) 56 | 57 | // if m has no other incoming edges then 58 | if len(g.adjacencyIn[m]) == 0 { 59 | // insert m into S 60 | S = append(S, m) 61 | } 62 | } 63 | } 64 | 65 | // if graph has edges then 66 | // return error (graph has at least one cycle) 67 | for _, out := range g.adjacencyOut { 68 | if len(out) > 0 { 69 | // We have cycles, so let's do cycle detection to give a better 70 | // error message. 71 | cycles := g.Cycles() 72 | panic(fmt.Sprintf("graph has cycles: %v", cycles)) 73 | } 74 | } 75 | 76 | return L 77 | } 78 | 79 | // TopoOrder is a topological ordering. 80 | type TopoOrder []Vertex 81 | 82 | // At returns a new TopoOrder that starts at the given vertex. 83 | // This returns a slice into the ordering so it is not safe to modify. 84 | func (t TopoOrder) At(v Vertex) TopoOrder { 85 | for i := 0; i < len(t); i++ { 86 | if t[i] == v { 87 | return t[i:] 88 | } 89 | } 90 | return nil 91 | } 92 | 93 | // Until returns a new TopoOrder that ends at (and includes) the given 94 | // vertex. This returns a slice into the ordering so it is not safe to modify. 95 | func (t TopoOrder) Until(v Vertex) TopoOrder { 96 | for i := 0; i < len(t); i++ { 97 | if t[i] == v { 98 | return t[:i] 99 | } 100 | } 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /internal/graph/path.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package graph 5 | 6 | // TopoShortestPath returns the shortest path information given the 7 | // topological sort of the graph L. L can be retrieved using any topological 8 | // sort algorithm such as KahnSort. 9 | // 10 | // The return value are two maps with the distance to and edge to information, 11 | // respectively. distTo maps the total distance from source to the given 12 | // vertex. edgeTo maps the previous edge to get to a vertex from source. 13 | func (g *Graph) TopoShortestPath(L TopoOrder) (distTo map[interface{}]int, edgeTo map[interface{}]Vertex) { 14 | /* 15 | Set the distance to the source to 0; 16 | Set the distances to all other vertices to infinity; 17 | For each vertex u in L 18 | - Walk through all neighbors v of u; 19 | - If dist(v) > dist(u) + w(u, v) 20 | - Set dist(v) <- dist(u) + w(u, v); 21 | */ 22 | 23 | // Set the distance to the source to 0; 24 | // Set the distances to all other vertices to infinity; 25 | // We don't actually set anything to "infinity" here since we can simulate 26 | // it by checking for existance in the map. 27 | distTo = map[interface{}]int{} 28 | edgeTo = map[interface{}]Vertex{} 29 | 30 | // For each vertex u in L 31 | for _, u := range L { 32 | uh := hashcode(u) 33 | 34 | // Walk through all neighbors v of u; 35 | for vh, weight := range g.adjacencyOut[uh] { 36 | // x = dist(u) + w(u, v) 37 | x := distTo[uh] + weight 38 | 39 | // If dist(v) > dist(u) + w(u, v) 40 | if _, ok := distTo[vh]; !ok || distTo[vh] > x { 41 | distTo[vh] = x 42 | edgeTo[vh] = u 43 | } 44 | } 45 | } 46 | 47 | return distTo, edgeTo 48 | } 49 | 50 | // EdgeToPath turns an "edge to" mapping into a vertex slice of the path 51 | // from some target. 52 | func (g *Graph) EdgeToPath(target Vertex, edgeTo map[interface{}]Vertex) []Vertex { 53 | var result []Vertex 54 | 55 | current := target 56 | for current != nil { 57 | result = append(result, current) 58 | current = edgeTo[hashcode(current)] 59 | } 60 | 61 | // Reverse it, since this puts the path in reverse order 62 | for left, right := 0, len(result)-1; left < right; left, right = left+1, right-1 { 63 | result[left], result[right] = result[right], result[left] 64 | } 65 | 66 | return result 67 | } 68 | -------------------------------------------------------------------------------- /internal/graph/path_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package graph 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestGraphTopoShortestPath(t *testing.T) { 13 | require := require.New(t) 14 | 15 | // This test is from the coursera page linked below. I chose to copy 16 | // something relatively well known and externally solved so I can be 100% 17 | // sure I got it right, plus to test cases where paths change multiple times. 18 | // https://www.coursera.org/lecture/algorithms-part2/edge-weighted-dags-6rxSt 19 | var g Graph 20 | g.Add(0) 21 | g.Add(1) 22 | g.Add(2) 23 | g.Add(3) 24 | g.Add(4) 25 | g.Add(5) 26 | g.Add(6) 27 | g.Add(7) 28 | g.AddEdgeWeighted(0, 1, 5) 29 | g.AddEdgeWeighted(0, 7, 8) 30 | g.AddEdgeWeighted(0, 4, 9) 31 | g.AddEdgeWeighted(1, 7, 4) 32 | g.AddEdgeWeighted(1, 3, 15) 33 | g.AddEdgeWeighted(1, 2, 12) 34 | g.AddEdgeWeighted(7, 2, 7) 35 | g.AddEdgeWeighted(7, 5, 6) 36 | g.AddEdgeWeighted(4, 7, 5) 37 | g.AddEdgeWeighted(4, 5, 4) 38 | g.AddEdgeWeighted(4, 6, 20) 39 | g.AddEdgeWeighted(5, 2, 1) 40 | g.AddEdgeWeighted(5, 6, 13) 41 | g.AddEdgeWeighted(2, 3, 3) 42 | g.AddEdgeWeighted(2, 6, 11) 43 | g.AddEdgeWeighted(3, 6, 9) 44 | 45 | sort := g.KahnSort() 46 | t.Logf("sorted: %#v", sort) 47 | 48 | distTo, edgeTo := g.TopoShortestPath(sort) 49 | require.Equal(0, distTo[0]) 50 | require.Equal(5, distTo[1]) 51 | require.Equal(14, distTo[2]) 52 | require.Equal(17, distTo[3]) 53 | require.Equal(9, distTo[4]) 54 | require.Equal(13, distTo[5]) 55 | require.Equal(25, distTo[6]) 56 | require.Equal(8, distTo[7]) 57 | 58 | require.Equal(nil, edgeTo[0]) 59 | require.Equal(0, edgeTo[1]) 60 | require.Equal(5, edgeTo[2]) 61 | require.Equal(2, edgeTo[3]) 62 | require.Equal(0, edgeTo[4]) 63 | require.Equal(4, edgeTo[5]) 64 | require.Equal(2, edgeTo[6]) 65 | require.Equal(0, edgeTo[7]) 66 | } 67 | -------------------------------------------------------------------------------- /internal/graph/tarjan.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package graph 5 | 6 | // Cycles returns all the detected cycles. This may not be fully exhaustive 7 | // since we use Tarjan's algoritm for strongly connected components to detect 8 | // cycles and this isn't guaranteed to find all cycles. 9 | func (g *Graph) Cycles() [][]Vertex { 10 | var cycles [][]Vertex 11 | for _, cycle := range g.StronglyConnected() { 12 | if len(cycle) > 1 { 13 | cycles = append(cycles, cycle) 14 | } 15 | } 16 | 17 | return cycles 18 | } 19 | 20 | // StronglyConnected returns the list of strongly connected components 21 | // within the Graph g. 22 | func (g *Graph) StronglyConnected() [][]Vertex { 23 | vs := g.Vertices() 24 | acct := sccAcct{ 25 | NextIndex: 1, 26 | VertexIndex: make(map[Vertex]int, len(vs)), 27 | } 28 | for _, v := range vs { 29 | // Recurse on any non-visited nodes 30 | if acct.VertexIndex[v] == 0 { 31 | stronglyConnected(&acct, g, v) 32 | } 33 | } 34 | return acct.SCC 35 | } 36 | 37 | func stronglyConnected(acct *sccAcct, g *Graph, v Vertex) int { 38 | // Initial vertex visit 39 | index := acct.visit(v) 40 | minIdx := index 41 | 42 | for _, target := range g.OutEdges(v) { 43 | targetIdx := acct.VertexIndex[target] 44 | 45 | // Recurse on successor if not yet visited 46 | if targetIdx == 0 { 47 | minIdx = min(minIdx, stronglyConnected(acct, g, target)) 48 | } else if acct.inStack(target) { 49 | // Check if the vertex is in the stack 50 | minIdx = min(minIdx, targetIdx) 51 | } 52 | } 53 | 54 | // Pop the strongly connected components off the stack if 55 | // this is a root vertex 56 | if index == minIdx { 57 | var scc []Vertex 58 | for { 59 | v2 := acct.pop() 60 | scc = append(scc, v2) 61 | if v2 == v { 62 | break 63 | } 64 | } 65 | 66 | acct.SCC = append(acct.SCC, scc) 67 | } 68 | 69 | return minIdx 70 | } 71 | 72 | func min(a, b int) int { 73 | if a <= b { 74 | return a 75 | } 76 | return b 77 | } 78 | 79 | // sccAcct is used ot pass around accounting information for 80 | // the StronglyConnectedComponents algorithm 81 | type sccAcct struct { 82 | NextIndex int 83 | VertexIndex map[Vertex]int 84 | Stack []Vertex 85 | SCC [][]Vertex 86 | } 87 | 88 | // visit assigns an index and pushes a vertex onto the stack 89 | func (s *sccAcct) visit(v Vertex) int { 90 | idx := s.NextIndex 91 | s.VertexIndex[v] = idx 92 | s.NextIndex++ 93 | s.push(v) 94 | return idx 95 | } 96 | 97 | // push adds a vertex to the stack 98 | func (s *sccAcct) push(n Vertex) { 99 | s.Stack = append(s.Stack, n) 100 | } 101 | 102 | // pop removes a vertex from the stack 103 | func (s *sccAcct) pop() Vertex { 104 | n := len(s.Stack) 105 | if n == 0 { 106 | return nil 107 | } 108 | vertex := s.Stack[n-1] 109 | s.Stack = s.Stack[:n-1] 110 | return vertex 111 | } 112 | 113 | // inStack checks if a vertex is in the stack 114 | func (s *sccAcct) inStack(needle Vertex) bool { 115 | for _, n := range s.Stack { 116 | if n == needle { 117 | return true 118 | } 119 | } 120 | return false 121 | } 122 | -------------------------------------------------------------------------------- /internal/graph/vertex.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package graph 5 | 6 | import "fmt" 7 | 8 | // Vertex can be anything. 9 | type Vertex interface{} 10 | 11 | // VertexHashable is an optional interface that can be implemented to specify 12 | // an alternate hash code for a Vertex. If this isnt implemented, Go interface 13 | // equality is used. 14 | type VertexHashable interface { 15 | Hashcode() interface{} 16 | } 17 | 18 | // VertexID returns the unique ID for a vertex. 19 | func VertexID(v Vertex) interface{} { 20 | return hashcode(v) 21 | } 22 | 23 | // VertexName returns the name of a vertex. 24 | func VertexName(v Vertex) string { 25 | switch v := v.(type) { 26 | case fmt.Stringer: 27 | return v.String() 28 | default: 29 | return fmt.Sprintf("%v", v) 30 | } 31 | } 32 | 33 | // hashcode returns the hashcode for a Vertex. 34 | func hashcode(v interface{}) interface{} { 35 | if h, ok := v.(VertexHashable); ok { 36 | return h.Hashcode() 37 | } 38 | 39 | return v 40 | } 41 | -------------------------------------------------------------------------------- /redefine.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package argmapper 5 | 6 | import ( 7 | "fmt" 8 | "reflect" 9 | "strings" 10 | 11 | "github.com/hashicorp/go-argmapper/internal/graph" 12 | "github.com/hashicorp/go-multierror" 13 | ) 14 | 15 | // Redefine returns a new func where the requirements are what is missing to 16 | // satisfy the original function given the arguments here. Therefore, args 17 | // may be incomplete, and this will return a function that only depends 18 | // on the missing arguments. 19 | // 20 | // Redefine also allows the usage of FilterInput and FilterOutput Arg 21 | // values. These can be used to further restrict what values can be provided 22 | // as an input or returned as an output, respectively. This can be used 23 | // for example to try to redefine a function to only take Go primitives. 24 | // In the case where Filter is used, converters must be specified that 25 | // enable going to and from filtered values. 26 | // 27 | // Currently, FilterOutput will just return an error if the functions 28 | // outputs don't match what is expected. In the future, we plan on enabling 29 | // FilterOutput to also map through converters to return the desired matches. 30 | // 31 | // If it is impossible to redefine the function according to the given 32 | // constraints, an error will be returned. 33 | func (f *Func) Redefine(opts ...Arg) (*Func, error) { 34 | // First we check the outputs since we currently only error if the outputs 35 | // do not match the filter. In the future, we'll do conversions here too. 36 | if err := f.redefineOutputs(opts...); err != nil { 37 | return nil, err 38 | } 39 | 40 | // Redefine our inputs to get the struct type that we'll take in the new function. 41 | inputStruct, err := f.redefineInputs(opts...) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | // Build our output type which just matches our function today. 47 | out := make([]reflect.Type, f.fn.Type().NumOut()) 48 | for i := range out { 49 | out[i] = f.fn.Type().Out(i) 50 | } 51 | 52 | // hasErr tells us whether out originally had an error output. We need 53 | // this to construct the proper return value in the dynamic func below. 54 | hasErr := true 55 | 56 | // If we don't have an error type, add that 57 | if len(out) == 0 || out[len(out)-1] != errType { 58 | out = append(out, errType) 59 | hasErr = false 60 | } 61 | 62 | // Build our function type and implementation. 63 | fnType := reflect.FuncOf([]reflect.Type{inputStruct}, out, false) 64 | fn := reflect.MakeFunc(fnType, func(args []reflect.Value) []reflect.Value { 65 | v := args[0] 66 | 67 | // Get our value set. Our args are guaranteed to be a struct. 68 | set, err := newValueSetFromStruct(inputStruct) 69 | if err != nil { 70 | panic(err) 71 | } 72 | 73 | // Copy our options 74 | callArgs := make([]Arg, len(opts)) 75 | copy(callArgs, opts) 76 | 77 | // Setup our values 78 | for name, f := range set.namedValues { 79 | callArgs = append(callArgs, Named(name, v.Field(f.index).Interface())) 80 | } 81 | for _, f := range set.typedValues { 82 | callArgs = append(callArgs, Typed(v.Field(f.index).Interface())) 83 | } 84 | 85 | // Call 86 | result := f.Call(callArgs...) 87 | 88 | // If we had an error, then we return the error. We always define 89 | // our new functions to return a final error type so set that and 90 | // return. 91 | if err := result.Err(); err != nil { 92 | retval := make([]reflect.Value, len(out)) 93 | for i, t := range out { 94 | retval[i] = reflect.Zero(t) 95 | } 96 | 97 | retval[len(retval)-1] = reflect.ValueOf(err) 98 | return retval 99 | } 100 | 101 | out := result.out 102 | if !hasErr { 103 | // If we didn't originally have an error value, then we 104 | // append the zero value since we always return an error 105 | // as the final result from this dynamic func. 106 | out = append(result.out, reflect.Zero(errType)) 107 | } 108 | 109 | return out 110 | }) 111 | 112 | return NewFunc(fn.Interface(), 113 | FuncName(f.Name()), // Preserve the name from the original func 114 | ) 115 | } 116 | 117 | // redefineInputs is called by Redefine to determine the input struct type 118 | // expected based on the Redefine arguments. This returns a struct type 119 | // (as reflect.Type) that represents the new input structure for the 120 | // redefined function. 121 | func (f *Func) redefineInputs(opts ...Arg) (reflect.Type, error) { 122 | builder, err := f.argBuilder(opts...) 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | // We have to tell our builder that we're redefining. This changes 128 | // how the graph is constructed slightly. 129 | builder.redefining = true 130 | 131 | // Get our log we'll use for logging 132 | log := builder.logger 133 | 134 | // Get our call graph 135 | g, vertexRoot, vertexF, vertexI, err := f.callGraph(builder) 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | // We go through the call graph and modify the functions to be no-ops 141 | // that just set the output values to zero values. This will let our 142 | // redefine process "call" each of our converters as if they work 143 | // perfectly and then we can determine what inputs are required by 144 | // just calling our target function and checking state.InputSet. 145 | for _, v := range g.Vertices() { 146 | switch v := v.(type) { 147 | case *funcVertex: 148 | // Copy the func since we're going to modify a field in it. 149 | fCopy := *v.Func 150 | v.Func = &fCopy 151 | 152 | // Modify the function to be a zero producing function. 153 | fCopy.fn = fCopy.zeroFunc() 154 | } 155 | } 156 | 157 | // Build our call state and attempt to reach our target which is our 158 | // function. This will recursively reach various conversion targets 159 | // as necessary. 160 | state := newCallState() 161 | if _, err := f.reachTarget(log, &g, vertexRoot, vertexF, state, true); err != nil { 162 | return nil, err 163 | } 164 | 165 | // Determine our map of inputs 166 | inputsProvided := map[interface{}]struct{}{} 167 | for _, v := range vertexI { 168 | inputsProvided[graph.VertexID(v)] = struct{}{} 169 | } 170 | 171 | // Build our required value 172 | var sf []reflect.StructField 173 | sf = append(sf, reflect.StructField{ 174 | Name: "Struct", 175 | Type: structMarkerType, 176 | Anonymous: true, 177 | }) 178 | for k, v := range state.InputSet { 179 | log.Trace("input", "value", v) 180 | if _, ok := inputsProvided[k]; ok { 181 | continue 182 | } 183 | 184 | switch v := v.(type) { 185 | case *valueVertex: 186 | sf = append(sf, reflect.StructField{ 187 | Name: strings.ToUpper(v.Name), 188 | Type: v.Type, 189 | }) 190 | 191 | case *typedArgVertex: 192 | sf = append(sf, reflect.StructField{ 193 | Name: fmt.Sprintf("V__Type_%d", len(sf)), 194 | Type: v.Type, 195 | Tag: reflect.StructTag(`argmapper:",typeOnly"`), 196 | }) 197 | } 198 | } 199 | 200 | return reflect.StructOf(sf), nil 201 | } 202 | 203 | // redefineOutputs redefines the outputs of the function in accordance 204 | // with FilterOutput. 205 | // 206 | // NOTE(mitchellh): today, we just validate the outputs. In the future, 207 | // we'll chain converters to reach a desired output. 208 | func (f *Func) redefineOutputs(opts ...Arg) error { 209 | builder, err := newArgBuilder(opts...) 210 | if err != nil { 211 | return err 212 | } 213 | 214 | if builder.filterOutput == nil { 215 | return nil 216 | } 217 | 218 | err = nil 219 | for _, v := range f.Output().Values() { 220 | if !builder.filterOutput(v) { 221 | err = multierror.Append(err, fmt.Errorf( 222 | "output %s does not satisfy output filter", v.String())) 223 | } 224 | } 225 | 226 | return err 227 | } 228 | 229 | // zeroFunc returns a function implementation that outputs the zero 230 | // value for all of its known outputs. This is used in the redefine graph 231 | // execution so we can determine what inputs are required to reach an output. 232 | func (f *Func) zeroFunc() reflect.Value { 233 | t := f.output 234 | fn := f.fn.Type() 235 | return reflect.MakeFunc(fn, func(args []reflect.Value) []reflect.Value { 236 | // Create our struct type and set all the fields to zero 237 | v := t.newStructValue() 238 | for _, f := range t.namedValues { 239 | v.Field(f.index).Set(reflect.Zero(f.Type)) 240 | } 241 | for _, f := range t.typedValues { 242 | v.Field(f.index).Set(reflect.Zero(f.Type)) 243 | } 244 | 245 | // Get our result. If we're expecting an error value, return nil for that. 246 | result := v.CallIn() 247 | if len(result) < fn.NumOut() { 248 | result = append(result, reflect.Zero(errType)) 249 | } 250 | 251 | return result 252 | }) 253 | } 254 | -------------------------------------------------------------------------------- /redefine_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package argmapper 5 | 6 | import ( 7 | "reflect" 8 | "strconv" 9 | "testing" 10 | 11 | "github.com/hashicorp/go-hclog" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func init() { 16 | hclog.L().SetLevel(hclog.Trace) 17 | } 18 | 19 | func TestFuncRedefine(t *testing.T) { 20 | cases := []struct { 21 | Name string 22 | Func interface{} 23 | Args []Arg 24 | Err string 25 | CallArgs []Arg 26 | CallResult []interface{} 27 | }{ 28 | { 29 | "all arguments satisfied", 30 | func(in struct { 31 | Struct 32 | 33 | A, B int 34 | }) int { 35 | return in.A + in.B 36 | }, 37 | []Arg{ 38 | Named("a", 12), 39 | Named("b", 24), 40 | }, 41 | "", 42 | []Arg{}, 43 | []interface{}{36}, 44 | }, 45 | 46 | { 47 | "missing a named argument", 48 | func(in struct { 49 | Struct 50 | 51 | A, B int 52 | }) int { 53 | return in.A + in.B 54 | }, 55 | []Arg{ 56 | Named("a", 12), 57 | }, 58 | "", 59 | []Arg{ 60 | Named("b", 24), 61 | }, 62 | []interface{}{36}, 63 | }, 64 | 65 | //----------------------------------------------------------- 66 | // FILTER INPUT 67 | 68 | { 69 | "only through strings direct", 70 | func(in struct { 71 | Struct 72 | 73 | A, B int 74 | }) int { 75 | return in.A + in.B 76 | }, 77 | []Arg{ 78 | Named("a", 12), 79 | Converter(func(v string) (int, error) { return strconv.Atoi(v) }), 80 | FilterInput(func(v Value) bool { return v.Type.Kind() == reflect.String }), 81 | }, 82 | "", 83 | []Arg{ 84 | Typed("24"), 85 | }, 86 | []interface{}{36}, 87 | }, 88 | 89 | { 90 | "only through strings with bidirectional converter", 91 | func(in struct { 92 | Struct 93 | 94 | A, B int 95 | }) int { 96 | return in.A + in.B 97 | }, 98 | []Arg{ 99 | Named("a", 12), 100 | Converter(func(v string) (int, error) { return strconv.Atoi(v) }), 101 | Converter(func(v int) string { return strconv.Itoa(v) }), 102 | FilterInput(func(v Value) bool { return v.Type.Kind() == reflect.String }), 103 | }, 104 | "", 105 | []Arg{ 106 | Typed("24"), 107 | }, 108 | []interface{}{36}, 109 | }, 110 | 111 | { 112 | "input with two possible converters", 113 | func(in struct { 114 | Struct 115 | 116 | A, B int 117 | }) int { 118 | return in.A + in.B 119 | }, 120 | []Arg{ 121 | Named("a", 12), 122 | Converter(func(v string) (int, error) { return strconv.Atoi(v) }), 123 | Converter(func(v []byte) string { return string(v) }), 124 | Converter(func(v string) []byte { return []byte(v) }), 125 | FilterInput(FilterOr( 126 | FilterType(reflect.TypeOf("")), 127 | )), 128 | }, 129 | "", 130 | []Arg{ 131 | Typed("24"), 132 | }, 133 | []interface{}{36}, 134 | }, 135 | 136 | { 137 | "unsatisfiable", 138 | func(in struct { 139 | Struct 140 | 141 | A, B int 142 | }) int { 143 | return in.A + in.B 144 | }, 145 | []Arg{ 146 | Named("a", 12), 147 | FilterInput(func(v Value) bool { return v.Type.Kind() == reflect.String }), 148 | }, 149 | `"b" (type: int)`, 150 | []Arg{}, 151 | nil, 152 | }, 153 | 154 | //----------------------------------------------------------- 155 | // FILTER OUTPUT 156 | 157 | { 158 | "satisfy output type", 159 | func(in struct { 160 | Struct 161 | 162 | A, B int 163 | }) int { 164 | return in.A + in.B 165 | }, 166 | []Arg{ 167 | Named("a", 12), 168 | Named("b", 24), 169 | FilterOutput(FilterType(reflect.TypeOf(int(0)))), 170 | }, 171 | "", 172 | []Arg{}, 173 | []interface{}{36}, 174 | }, 175 | 176 | { 177 | "satisfy output type with error", 178 | func(in struct { 179 | Struct 180 | 181 | A, B int 182 | }) (int, error) { 183 | return in.A + in.B, nil 184 | }, 185 | []Arg{ 186 | Named("a", 12), 187 | Named("b", 24), 188 | FilterOutput(FilterType(reflect.TypeOf(int(0)))), 189 | }, 190 | "", 191 | []Arg{}, 192 | []interface{}{36}, 193 | }, 194 | 195 | { 196 | "satisfy output type with only error result", 197 | func(in struct { 198 | Struct 199 | 200 | A, B int 201 | }) error { 202 | return nil 203 | }, 204 | []Arg{ 205 | Named("a", 12), 206 | Named("b", 24), 207 | FilterOutput(FilterType(reflect.TypeOf(int(0)))), 208 | }, 209 | "", 210 | []Arg{}, 211 | []interface{}{nil}, 212 | }, 213 | 214 | { 215 | "fail to satisfy output type", 216 | func(in struct { 217 | Struct 218 | 219 | A, B int 220 | }) int { 221 | return in.A + in.B 222 | }, 223 | []Arg{ 224 | Named("a", 12), 225 | Named("b", 24), 226 | FilterOutput(FilterType(reflect.TypeOf(string("")))), 227 | }, 228 | "output type: int", 229 | nil, 230 | nil, 231 | }, 232 | } 233 | 234 | for _, tt := range cases { 235 | t.Run(tt.Name, func(t *testing.T) { 236 | require := require.New(t) 237 | 238 | f, err := NewFunc(tt.Func) 239 | require.NoError(err) 240 | 241 | redefined, err := f.Redefine(tt.Args...) 242 | if tt.Err == "" { 243 | require.NoError(err) 244 | } else { 245 | require.Error(err) 246 | require.Contains(err.Error(), tt.Err) 247 | return 248 | } 249 | 250 | result := redefined.Call(tt.CallArgs...) 251 | require.NoError(result.Err()) 252 | for i, out := range tt.CallResult { 253 | require.Equal(out, result.Out(i)) 254 | } 255 | }) 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /result.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package argmapper 5 | 6 | import "reflect" 7 | 8 | // Result is returned from a Call with the results of the function call. 9 | // 10 | // This structure lets you access multiple results values. If the function 11 | // call had a final return value type "error", this is treated specially 12 | // and is present via the Err call and not via Out. 13 | type Result struct { 14 | out []reflect.Value 15 | buildErr error 16 | } 17 | 18 | // resultError returns a Result with an error. 19 | func resultError(err error) Result { 20 | return Result{buildErr: err} 21 | } 22 | 23 | // Err returns any error that occurred as part of the call. This can 24 | // be an error in the process of calling or it can be an error from the 25 | // result of the call. 26 | func (r *Result) Err() error { 27 | if r.buildErr != nil { 28 | return r.buildErr 29 | } 30 | 31 | if len(r.out) > 0 { 32 | final := r.out[len(r.out)-1] 33 | if final.IsValid() && final.Type() == errType { 34 | if err := final.Interface(); err != nil { 35 | return err.(error) 36 | } 37 | } 38 | 39 | return nil 40 | } 41 | 42 | return nil 43 | } 44 | 45 | // Out returns the i'th result (zero-indexed) of the function. This will 46 | // panic if i >= Len so for safety all calls to Out should check Len. 47 | // 48 | // Similar to Len, Out does not include any final "error" type. This can only 49 | // be accessed using Err(). 50 | func (r *Result) Out(i int) interface{} { 51 | return r.out[i].Interface() 52 | } 53 | 54 | // Len returns the number of outputs, excluding any final error output. 55 | // 56 | // Len does not include the "error" type if it was the final output type. 57 | // For example, a function returning (error), (int, error), (int, bool, error) 58 | // would have a length of 0, 1, and 2 respectively. 59 | func (r *Result) Len() int { 60 | result := len(r.out) 61 | if r.hasError() { 62 | result -= 1 63 | } 64 | 65 | return result 66 | } 67 | 68 | func (r *Result) hasError() bool { 69 | if len(r.out) == 0 { 70 | return false 71 | } 72 | 73 | final := r.out[len(r.out)-1] 74 | return final.Type() == errType 75 | } 76 | -------------------------------------------------------------------------------- /struct.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package argmapper 5 | 6 | import "reflect" 7 | 8 | // Struct should be embedded into any struct where the parameters are 9 | // populated. This lets argmapper differentiate between arguments 10 | // where you want the full struct provided or fields within the struct. 11 | // 12 | // Example: 13 | // 14 | // type MyParams { 15 | // argmapper.Struct 16 | // 17 | // // A and B will be populated through injection. 18 | // A, B int 19 | // } 20 | // 21 | // If the embedded Struct was left out, argmapper would look for 22 | // a full MyParams type to inject. 23 | // 24 | // Named Parameters 25 | // 26 | // By default, the field name is the name of the parameter. In the 27 | // example above, MyParams expects parameters named "A" and "B", both of 28 | // type int. 29 | // 30 | // Parameter names are case insensitive. 31 | // 32 | // Parameters can be renamed using a struct tag. The example below 33 | // renames the field "A" to "B". 34 | // 35 | // type MyParams { 36 | // argmapper.Struct 37 | // 38 | // A int `argmapper:"B"` 39 | // } 40 | // 41 | // Typed Parameters 42 | // 43 | // A field in the struct can be marked as typed only using struct tags. 44 | // The field name of a typed field is ignored and argmapper will match it 45 | // to any matching type. 46 | // 47 | // type MyParams { 48 | // argmapper.Struct 49 | // 50 | // A int `argmapper:",typeOnly"` 51 | // } 52 | // 53 | // Note the comma before the "typeOnly" string. The comma is necessary 54 | // so tell argmapper you're setting an option versus renaming a field. 55 | type Struct struct { 56 | structInterface 57 | } 58 | 59 | // structInterface so that users can't just embed any struct{} type. 60 | type structInterface interface { 61 | argmapperStruct() 62 | } 63 | 64 | // isStruct returns true if the given type is a struct that embeds our 65 | // struct marker. 66 | func isStruct(t reflect.Type) bool { 67 | for t.Kind() == reflect.Ptr { 68 | t = t.Elem() 69 | } 70 | 71 | if t.Kind() != reflect.Struct { 72 | return false 73 | } 74 | 75 | for i := 0; i < t.NumField(); i++ { 76 | if isStructField(t.Field(i)) { 77 | return true 78 | } 79 | } 80 | 81 | return false 82 | } 83 | 84 | // isStructField returns true if the given struct field is our struct marker. 85 | func isStructField(f reflect.StructField) bool { 86 | return f.Anonymous && f.Type == structMarkerType 87 | } 88 | 89 | var structMarkerType = reflect.TypeOf((*Struct)(nil)).Elem() 90 | -------------------------------------------------------------------------------- /struct_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package argmapper 5 | 6 | import ( 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestIsStruct(t *testing.T) { 14 | cases := []struct { 15 | Name string 16 | Test interface{} 17 | Expected bool 18 | }{ 19 | { 20 | "primitive", 21 | 7, 22 | false, 23 | }, 24 | 25 | { 26 | "struct embeds", 27 | struct { 28 | Struct 29 | }{}, 30 | true, 31 | }, 32 | } 33 | 34 | for _, tt := range cases { 35 | t.Run(tt.Name, func(t *testing.T) { 36 | require := require.New(t) 37 | 38 | actual := isStruct(reflect.TypeOf(tt.Test)) 39 | require.Equal(tt.Expected, actual) 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /value_set.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package argmapper 5 | 6 | import ( 7 | "fmt" 8 | "reflect" 9 | "strings" 10 | 11 | "github.com/hashicorp/go-argmapper/internal/graph" 12 | ) 13 | 14 | //go:generate stringer -type=ValueKind 15 | 16 | // ValueSet tracks the values either accepted or returned as part of 17 | // a function or converter. 18 | // 19 | // Internally, argmapper converts all functions to a signature of 20 | // `func(Struct) (Struct, error)`. This lets the internals simplify a lot 21 | // by expecting to only set struct fields. On the edges (when calling functions 22 | // or returning values) we convert to and from the true expected arguments. 23 | type ValueSet struct { 24 | // structType is a struct that contains all the settable values. 25 | structType reflect.Type 26 | 27 | // structPointers is the number of pointers that wrap structType. 28 | // We use this to construct the correct argument type. Note we only 29 | // support one pointer today, so this will be at most "1" but making it 30 | // an int for the future. 31 | structPointers uint8 32 | 33 | // values is the set of values that this ValueSet contains. namedValues, 34 | // typedValues, etc. are convenience maps for looking up values more 35 | // easily. 36 | values []*Value 37 | namedValues map[string]*Value 38 | typedValues map[reflect.Type]*Value 39 | 40 | // isLifted is if this represents a lifted struct. A lifted struct 41 | // is one where we automatically converted flat argument lists to 42 | // structs. 43 | isLifted bool 44 | } 45 | 46 | // Value represents an input or output of a Func. In normal operation, you 47 | // do not need to interact with Value objects directly. This structure 48 | // is exposed for users who are trying to introspect on functions or manually 49 | // build functions. This is an advanced operation. 50 | // 51 | // A Value represents multiple types of values depending on what fields are 52 | // set. Please read the documentation carefully and use the exported methods 53 | // to assist with checking value types. 54 | type Value struct { 55 | // valueInternal is the internal information for the value. This is only 56 | // set and used by ValueSet. 57 | valueInternal 58 | 59 | // Name is the name of the value. This may be empty if this is a type-only 60 | // value. If the name is set, then we will satisfy this input with an arg 61 | // with this name and type. 62 | Name string 63 | 64 | // Type is the type of the value. This must be set. 65 | Type reflect.Type 66 | 67 | // Subtype is a key that specifies a unique "subtype" for the type. 68 | // This can be used to identify dynamic values such as protobuf Any types 69 | // where the full type isn't available. This is optional. For full details 70 | // on subtype matching see the package docs. 71 | Subtype string 72 | 73 | // Value is the known value. This is only ever set if using Func.Redefine 74 | // with an input that was given. Otherwise, this value is invalid. 75 | Value reflect.Value 76 | } 77 | 78 | // ValueKind is returned by Value.Kind to designate what kind of value this is: 79 | // a value expecting a type and name, a value with just type matching, etc. 80 | type ValueKind uint 81 | 82 | const ( 83 | ValueInvalid ValueKind = iota 84 | ValueNamed 85 | ValueTyped 86 | ) 87 | 88 | type valueInternal struct { 89 | // index is the struct field index for the ValueSet on which to set values. 90 | index int 91 | } 92 | 93 | // NewValueSet creates a ValueSet from a list of expected values. 94 | // 95 | // This is primarily used alongside BuildFunc to dynamically build a Func. 96 | func NewValueSet(vs []Value) (*ValueSet, error) { 97 | // Build a dynamic struct based on the value list. The struct is 98 | // only used for input/output mapping. 99 | var sf []reflect.StructField 100 | sf = append(sf, reflect.StructField{ 101 | Name: "Struct", 102 | Type: structMarkerType, 103 | Anonymous: true, 104 | }) 105 | for i, v := range vs { 106 | if isStruct(v.Type) { 107 | return nil, fmt.Errorf("can't have argmapper.Struct values with custom ValueSet building") 108 | } 109 | 110 | // TODO(mitchellh): error on duplicate names, types 111 | 112 | // Build our tag. 113 | tags := []string{""} 114 | if v.Name == "" { 115 | tags = append(tags, "typeOnly") 116 | } 117 | if v.Subtype != "" { 118 | tags = append(tags, fmt.Sprintf("subtype=%s", v.Subtype)) 119 | } 120 | tag := reflect.StructTag(fmt.Sprintf(`argmapper:"%s"`, strings.Join(tags, ","))) 121 | 122 | switch v.Kind() { 123 | case ValueNamed: 124 | sf = append(sf, reflect.StructField{ 125 | Name: strings.ToUpper(v.Name), 126 | Type: v.Type, 127 | Tag: tag, 128 | }) 129 | 130 | case ValueTyped: 131 | sf = append(sf, reflect.StructField{ 132 | Name: fmt.Sprintf("V__Type_%d", i), 133 | Type: v.Type, 134 | Tag: tag, 135 | }) 136 | 137 | default: 138 | panic("unknown kind") 139 | } 140 | } 141 | 142 | return newValueSetFromStruct(reflect.StructOf(sf)) 143 | } 144 | 145 | func newValueSet(count int, get func(int) reflect.Type) (*ValueSet, error) { 146 | // If there are no arguments, then return an empty value set. 147 | if count == 0 { 148 | return &ValueSet{}, nil 149 | } 150 | 151 | // If we have exactly one argument, let's check if its a struct. If 152 | // it is then we treat it as the full value. 153 | if count == 1 { 154 | if t := get(0); isStruct(t) { 155 | return newValueSetFromStruct(t) 156 | } 157 | } 158 | 159 | // We need to lift the arguments into a "struct". 160 | var sf []reflect.StructField 161 | for i := 0; i < count; i++ { 162 | t := get(i) 163 | if isStruct(t) { 164 | return nil, fmt.Errorf("can't mix argmapper.Struct and non-struct values") 165 | } 166 | 167 | sf = append(sf, reflect.StructField{ 168 | Name: fmt.Sprintf("V__Type_%d", i), 169 | Type: t, 170 | Tag: reflect.StructTag(`argmapper:",typeOnly"`), 171 | }) 172 | } 173 | 174 | t, err := newValueSetFromStruct(reflect.StructOf(sf)) 175 | if err != nil { 176 | return nil, err 177 | } 178 | 179 | t.isLifted = true 180 | return t, nil 181 | } 182 | 183 | func newValueSetFromStruct(typ reflect.Type) (*ValueSet, error) { 184 | // Unwrap any pointers around our struct type and count the number of 185 | // pointer derefs. We need to know the count to reconstruct it later. 186 | var ptrCount uint8 187 | for typ.Kind() == reflect.Ptr { 188 | typ = typ.Elem() 189 | ptrCount++ 190 | } 191 | if ptrCount > 1 { 192 | return nil, fmt.Errorf("struct argument can at most be a single pointer") 193 | } 194 | 195 | // Verify our value is a struct 196 | if typ.Kind() != reflect.Struct { 197 | return nil, fmt.Errorf("struct expected, got %s", typ.Kind()) 198 | } 199 | 200 | // We will accumulate our results here 201 | result := &ValueSet{ 202 | structType: typ, 203 | structPointers: ptrCount, 204 | values: []*Value{}, 205 | namedValues: map[string]*Value{}, 206 | typedValues: map[reflect.Type]*Value{}, 207 | } 208 | 209 | // Go through the fields and record them all 210 | for i := 0; i < typ.NumField(); i++ { 211 | sf := typ.Field(i) 212 | 213 | // Ignore unexported fields and our struct marker 214 | if sf.PkgPath != "" || isStructField(sf) { 215 | continue 216 | } 217 | 218 | // name is the name of the value to inject. 219 | name := sf.Name 220 | 221 | // Parse out the tag if there is one 222 | var options map[string]string 223 | if tag := sf.Tag.Get("argmapper"); tag != "" { 224 | parts := strings.Split(tag, ",") 225 | 226 | // If we have a name set, then override the name 227 | if parts[0] != "" { 228 | name = parts[0] 229 | } 230 | 231 | // If we have fields set after the comma, then we want to 232 | // parse those as values. 233 | options = make(map[string]string) 234 | for _, v := range parts[1:] { 235 | idx := strings.Index(v, "=") 236 | if idx == -1 { 237 | options[v] = "" 238 | } else { 239 | options[v[:idx]] = v[idx+1:] 240 | } 241 | } 242 | } 243 | 244 | // Name is always lowercase 245 | name = strings.ToLower(name) 246 | if _, ok := options["typeOnly"]; ok { 247 | name = "" 248 | } 249 | 250 | // Record it 251 | value := Value{ 252 | Name: name, 253 | Type: sf.Type, 254 | Subtype: options["subtype"], 255 | valueInternal: valueInternal{ 256 | index: i, 257 | }, 258 | } 259 | 260 | result.values = append(result.values, &value) 261 | switch value.Kind() { 262 | case ValueNamed: 263 | result.namedValues[value.Name] = &value 264 | 265 | case ValueTyped: 266 | result.typedValues[value.Type] = &value 267 | } 268 | } 269 | 270 | return result, nil 271 | } 272 | 273 | // Values returns the values in this ValueSet. This does not return 274 | // pointers so any modifications to the values will not impact any values 275 | // in this set. Please call Named, Typed, etc. directly to make modifications. 276 | func (vs *ValueSet) Values() []Value { 277 | result := make([]Value, len(vs.values)) 278 | for i, v := range vs.values { 279 | result[i] = *v 280 | } 281 | return result 282 | } 283 | 284 | // Args returns all of the values in this ValueSet as a slice of Arg to 285 | // make it easier to pass to Call. This is equivalent to iterating over 286 | // the Values result and accumulating Arg results. 287 | func (vs *ValueSet) Args() []Arg { 288 | vals := vs.Values() 289 | args := make([]Arg, len(vals)) 290 | for i, v := range vals { 291 | args[i] = v.Arg() 292 | } 293 | return args 294 | } 295 | 296 | // Named returns a pointer to the value with the given name, or nil if 297 | // it doesn't exist. 298 | func (vs *ValueSet) Named(n string) *Value { 299 | return vs.namedValues[n] 300 | } 301 | 302 | // Typed returns a pointer to the value with the given type, or nil 303 | // if it doesn't exist. If there is no typed value directly, a random 304 | // type with the matching subtype will be chosen. If you want an exact 305 | // match with no subtype, use TypedSubtype. 306 | func (vs *ValueSet) Typed(t reflect.Type) *Value { 307 | // TODO: subtype 308 | return vs.typedValues[t] 309 | } 310 | 311 | // TypedSubtype returns a pointer to the value that matches the type 312 | // and subtype exactly. 313 | func (vs *ValueSet) TypedSubtype(t reflect.Type, st string) *Value { 314 | for _, v := range vs.values { 315 | if v.Type == t && v.Subtype == st { 316 | return v 317 | } 318 | } 319 | 320 | return nil 321 | } 322 | 323 | // Signature returns the type signature that this ValueSet will map to/from. 324 | // This is used for making dynamic types with reflect.FuncOf to take or return 325 | // this valueset. 326 | func (vs *ValueSet) Signature() []reflect.Type { 327 | // If our value set is nil then we have no arguments. 328 | if vs == nil { 329 | return nil 330 | } 331 | 332 | if !vs.lifted() { 333 | // This happens if the value has no values at all. In this case, 334 | // our signature is also empty. 335 | if vs.structType == nil { 336 | return nil 337 | } 338 | 339 | return []reflect.Type{vs.structType} 340 | } 341 | 342 | result := make([]reflect.Type, len(vs.typedValues)) 343 | for _, v := range vs.typedValues { 344 | result[v.index] = v.Type 345 | } 346 | 347 | return result 348 | } 349 | 350 | // SignatureValues returns the values that match the Signature type list, 351 | // based on the values set in this set. If a value isn't set, the zero 352 | // value is used. 353 | func (vs *ValueSet) SignatureValues() []reflect.Value { 354 | // If typ is nil then there is no values 355 | if vs == nil || vs.structType == nil { 356 | return nil 357 | } 358 | 359 | // If we're lifted, we just return directly based on values 360 | if vs.lifted() { 361 | result := make([]reflect.Value, len(vs.typedValues)) 362 | for _, v := range vs.typedValues { 363 | result[v.index] = v.valueOrZero() 364 | } 365 | 366 | return result 367 | } 368 | 369 | // Not lifted, meaning we return a struct 370 | structOut := reflect.New(vs.structType).Elem() 371 | for _, f := range vs.values { 372 | structOut.Field(f.index).Set(f.valueOrZero()) 373 | } 374 | 375 | return []reflect.Value{structOut} 376 | } 377 | 378 | // FromSignature sets the values in this ValueSet based on the values list. 379 | // The values list must match the type signature returned from vs.Signature. 380 | // This usually comes from calling a function directly. 381 | func (vs *ValueSet) FromSignature(values []reflect.Value) error { 382 | // If we're lifted, then first set the values onto the struct. 383 | if vs.lifted() { 384 | // If we are lifted, then we need to translate the output arguments 385 | // to their proper types in a struct. 386 | structOut := reflect.New(vs.structType).Elem() 387 | for _, f := range vs.typedValues { 388 | structOut.Field(f.index).Set(values[f.index]) 389 | } 390 | 391 | values = []reflect.Value{structOut} 392 | } 393 | 394 | // Get our first result which should be our struct 395 | structVal := values[0] 396 | for i, v := range vs.values { 397 | vs.values[i].Value = structVal.Field(v.index) 398 | } 399 | 400 | return nil 401 | } 402 | 403 | // FromResult sets the values in this set based on a Result. This will 404 | // return an error if the result represents an error. 405 | func (vs *ValueSet) FromResult(r Result) error { 406 | if err := r.Err(); err != nil { 407 | return err 408 | } 409 | 410 | return vs.FromSignature(r.out) 411 | } 412 | 413 | // New returns a new structValue that can be used for value population. 414 | func (t *ValueSet) newStructValue() *structValue { 415 | result := &structValue{typ: t} 416 | if t.structType != nil { 417 | result.value = reflect.New(t.structType).Elem() 418 | } 419 | 420 | return result 421 | } 422 | 423 | // lifted returns true if this field is lifted. 424 | func (t *ValueSet) lifted() bool { 425 | return t.isLifted 426 | } 427 | 428 | func (t *ValueSet) empty() bool { 429 | return t.structType == nil || len(t.values) == 0 430 | } 431 | 432 | // result takes the result that matches this struct type and adapts it 433 | // if necessary (if the struct type is lifted or so on). 434 | func (t *ValueSet) result(r Result) Result { 435 | // If we aren't lifted, we return the direct struct. We have to unwrap 436 | // any pointers. We know this to be true already since we analyzed the 437 | // function earlier. 438 | if !t.lifted() { 439 | for i := uint8(0); i < t.structPointers; i++ { 440 | r.out[0] = r.out[0].Elem() 441 | } 442 | 443 | // A nil result is equivalent to zero values, allocate the struct. 444 | // This happens if there are pointer results and the user returns nil. 445 | if !r.out[0].IsValid() { 446 | r.out[0] = reflect.New(t.structType).Elem() 447 | } 448 | 449 | return r 450 | } 451 | 452 | // If we are lifted, then we need to translate the output arguments 453 | // to their proper types in a struct. 454 | structOut := reflect.New(t.structType).Elem() 455 | for _, f := range t.typedValues { 456 | structOut.Field(f.index).Set(r.out[f.index]) 457 | } 458 | 459 | r.out = []reflect.Value{structOut} 460 | return r 461 | } 462 | 463 | func newValueFromVertex(v graph.Vertex) *Value { 464 | switch v := v.(type) { 465 | case *valueVertex: 466 | return &Value{ 467 | Name: v.Name, 468 | Type: v.Type, 469 | Subtype: v.Subtype, 470 | Value: v.Value, 471 | } 472 | 473 | case *typedOutputVertex: 474 | return &Value{ 475 | Type: v.Type, 476 | Subtype: v.Subtype, 477 | Value: v.Value, 478 | } 479 | } 480 | 481 | return nil 482 | } 483 | 484 | // Arg returns an Arg that can be used with Func.Call to send this value. 485 | // This only works if the Value's Value field is set. 486 | func (v *Value) Arg() Arg { 487 | switch v.Kind() { 488 | case ValueNamed: 489 | return NamedSubtype(v.Name, v.Value.Interface(), v.Subtype) 490 | 491 | case ValueTyped: 492 | return TypedSubtype(v.Value.Interface(), v.Subtype) 493 | 494 | default: 495 | panic("unknown kind: " + v.Kind().String()) 496 | } 497 | } 498 | 499 | // Kind returns the ValueKind that this Value represents. 500 | func (v *Value) Kind() ValueKind { 501 | if v.Name != "" { 502 | return ValueNamed 503 | } 504 | 505 | return ValueTyped 506 | } 507 | 508 | func (v *Value) String() string { 509 | switch v.Kind() { 510 | case ValueNamed: 511 | if v.Subtype == "" { 512 | return fmt.Sprintf("name: %q (type: %s)", v.Name, v.Type) 513 | } else { 514 | return fmt.Sprintf("name: %q (type: %s, subtype: %s)", v.Name, v.Type, v.Subtype) 515 | } 516 | 517 | case ValueTyped: 518 | if v.Subtype == "" { 519 | return fmt.Sprintf("type: %s", v.Type) 520 | } else { 521 | return fmt.Sprintf("type: %s (subtype: %s)", v.Type, v.Subtype) 522 | } 523 | 524 | default: 525 | return fmt.Sprintf("%#v", v) 526 | } 527 | } 528 | 529 | func (v *Value) valueOrZero() reflect.Value { 530 | if !v.Value.IsValid() { 531 | return reflect.Zero(v.Type) 532 | } 533 | 534 | return v.Value 535 | } 536 | 537 | func (v *Value) vertex() graph.Vertex { 538 | switch v.Kind() { 539 | case ValueNamed: 540 | return &valueVertex{ 541 | Name: v.Name, 542 | Type: v.Type, 543 | Subtype: v.Subtype, 544 | } 545 | 546 | case ValueTyped: 547 | return &typedArgVertex{ 548 | Type: v.Type, 549 | Subtype: v.Subtype, 550 | } 551 | 552 | default: 553 | panic(fmt.Sprintf("unknown value kind: %s", v.Kind())) 554 | } 555 | } 556 | 557 | type structValue struct { 558 | typ *ValueSet 559 | value reflect.Value 560 | } 561 | 562 | func (v *structValue) Field(idx int) reflect.Value { 563 | return v.value.Field(idx) 564 | } 565 | 566 | func (v *structValue) CallIn() []reflect.Value { 567 | // If typ is nil then there is no inputs 568 | if v.typ.structType == nil { 569 | return nil 570 | } 571 | 572 | // If this is not lifted, return it as-is. 573 | if !v.typ.lifted() { 574 | // We do need to wrap the value in some pointers 575 | val := v.value 576 | for i := uint8(0); i < v.typ.structPointers; i++ { 577 | val = val.Addr() 578 | } 579 | 580 | return []reflect.Value{val} 581 | } 582 | 583 | // This is lifted, so we need to unpack them in order. 584 | result := make([]reflect.Value, len(v.typ.typedValues)) 585 | for _, f := range v.typ.typedValues { 586 | result[f.index] = v.value.Field(f.index) 587 | } 588 | 589 | return result 590 | } 591 | -------------------------------------------------------------------------------- /valuekind_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=ValueKind"; DO NOT EDIT. 2 | 3 | package argmapper 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[ValueInvalid-0] 12 | _ = x[ValueNamed-1] 13 | _ = x[ValueTyped-2] 14 | } 15 | 16 | const _ValueKind_name = "ValueInvalidValueNamedValueTyped" 17 | 18 | var _ValueKind_index = [...]uint8{0, 12, 22, 32} 19 | 20 | func (i ValueKind) String() string { 21 | if i >= ValueKind(len(_ValueKind_index)-1) { 22 | return "ValueKind(" + strconv.FormatInt(int64(i), 10) + ")" 23 | } 24 | return _ValueKind_name[_ValueKind_index[i]:_ValueKind_index[i+1]] 25 | } 26 | --------------------------------------------------------------------------------