├── .devcontainer └── devcontainer.json ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .release-please-manifest.json ├── .stats.yml ├── Brewfile ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── address.go ├── address_test.go ├── aliases.go ├── api.md ├── app.go ├── app_test.go ├── card.go ├── card_test.go ├── cart.go ├── cart_test.go ├── client.go ├── client_test.go ├── email.go ├── email_test.go ├── examples └── .keep ├── field.go ├── go.mod ├── go.sum ├── internal ├── apierror │ └── apierror.go ├── apiform │ ├── encoder.go │ ├── form.go │ ├── form_test.go │ └── tag.go ├── apijson │ ├── decoder.go │ ├── encoder.go │ ├── field.go │ ├── field_test.go │ ├── json_test.go │ ├── port.go │ ├── port_test.go │ ├── registry.go │ └── tag.go ├── apiquery │ ├── encoder.go │ ├── query.go │ ├── query_test.go │ └── tag.go ├── param │ └── field.go ├── requestconfig │ └── requestconfig.go ├── testutil │ └── testutil.go └── version.go ├── lib └── .keep ├── option ├── middleware.go └── requestoption.go ├── order.go ├── order_test.go ├── product.go ├── product_test.go ├── profile.go ├── profile_test.go ├── release-please-config.json ├── scripts ├── bootstrap ├── format ├── lint ├── mock └── test ├── subscription.go ├── subscription_test.go ├── token.go ├── token_test.go ├── usage_test.go ├── view.go └── view_test.go /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/debian 3 | { 4 | "name": "Development", 5 | "image": "mcr.microsoft.com/devcontainers/go:1.23-bookworm", 6 | "postCreateCommand": "go mod tidy" 7 | } 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches-ignore: 5 | - 'generated' 6 | - 'codegen/**' 7 | - 'integrated/**' 8 | - 'stl-preview-head/**' 9 | - 'stl-preview-base/**' 10 | pull_request: 11 | branches-ignore: 12 | - 'stl-preview-head/**' 13 | - 'stl-preview-base/**' 14 | 15 | jobs: 16 | lint: 17 | timeout-minutes: 10 18 | name: lint 19 | runs-on: ${{ github.repository == 'stainless-sdks/terminal-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} 20 | if: github.event_name == 'push' || github.event.pull_request.head.repo.fork 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Setup go 26 | uses: actions/setup-go@v5 27 | with: 28 | go-version-file: ./go.mod 29 | 30 | - name: Run lints 31 | run: ./scripts/lint 32 | test: 33 | timeout-minutes: 10 34 | name: test 35 | runs-on: ${{ github.repository == 'stainless-sdks/terminal-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} 36 | if: github.event_name == 'push' || github.event.pull_request.head.repo.fork 37 | steps: 38 | - uses: actions/checkout@v4 39 | 40 | - name: Setup go 41 | uses: actions/setup-go@v5 42 | with: 43 | go-version-file: ./go.mod 44 | 45 | - name: Bootstrap 46 | run: ./scripts/bootstrap 47 | 48 | - name: Run tests 49 | run: ./scripts/test 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .prism.log 2 | codegen.log 3 | Brewfile.lock.json 4 | .idea/ 5 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "1.16.3" 3 | } -------------------------------------------------------------------------------- /.stats.yml: -------------------------------------------------------------------------------- 1 | configured_endpoints: 37 2 | openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/terminal%2Fterminal-84ef614abb971e8673a8639face21c77dc72cc8f1b246e84796afcc7057e6b1b.yml 3 | openapi_spec_hash: d08a15c87914e11038f240f0d25d09e2 4 | config_hash: 74c1f4230ff802d4282aec9f2cf9e2d9 5 | -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | brew "go" 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Setting up the environment 2 | 3 | To set up the repository, run: 4 | 5 | ```sh 6 | $ ./scripts/bootstrap 7 | $ ./scripts/build 8 | ``` 9 | 10 | This will install all the required dependencies and build the SDK. 11 | 12 | You can also [install go 1.22+ manually](https://go.dev/doc/install). 13 | 14 | ## Modifying/Adding code 15 | 16 | Most of the SDK is generated code. Modifications to code will be persisted between generations, but may 17 | result in merge conflicts between manual patches and changes from the generator. The generator will never 18 | modify the contents of the `lib/` and `examples/` directories. 19 | 20 | ## Adding and running examples 21 | 22 | All files in the `examples/` directory are not modified by the generator and can be freely edited or added to. 23 | 24 | ```go 25 | # add an example to examples//main.go 26 | 27 | package main 28 | 29 | func main() { 30 | // ... 31 | } 32 | ``` 33 | 34 | ```sh 35 | $ go run ./examples/ 36 | ``` 37 | 38 | ## Using the repository from source 39 | 40 | To use a local version of this library from source in another project, edit the `go.mod` with a replace 41 | directive. This can be done through the CLI with the following: 42 | 43 | ```sh 44 | $ go mod edit -replace github.com/terminaldotshop/terminal-sdk-go=/path/to/terminal-sdk-go 45 | ``` 46 | 47 | ## Running tests 48 | 49 | Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. 50 | 51 | ```sh 52 | # you will need npm installed 53 | $ npx prism mock path/to/your/openapi.yml 54 | ``` 55 | 56 | ```sh 57 | $ ./scripts/test 58 | ``` 59 | 60 | ## Formatting 61 | 62 | This library uses the standard gofmt code formatter: 63 | 64 | ```sh 65 | $ ./scripts/format 66 | ``` 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2025 Terminal 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting Security Issues 4 | 5 | This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken. 6 | 7 | To report a security issue, please contact the Stainless team at security@stainless.com. 8 | 9 | ## Responsible Disclosure 10 | 11 | We appreciate the efforts of security researchers and individuals who help us maintain the security of 12 | SDKs we generate. If you believe you have found a security vulnerability, please adhere to responsible 13 | disclosure practices by allowing us a reasonable amount of time to investigate and address the issue 14 | before making any information public. 15 | 16 | ## Reporting Non-SDK Related Security Issues 17 | 18 | If you encounter security issues that are not directly related to SDKs but pertain to the services 19 | or products provided by Terminal, please follow the respective company's security reporting guidelines. 20 | 21 | ### Terminal Terms and Policies 22 | 23 | Please contact dev@terminal.com for any questions or concerns regarding the security of our services. 24 | 25 | --- 26 | 27 | Thank you for helping us keep the SDKs and systems they interact with secure. 28 | -------------------------------------------------------------------------------- /address.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package githubcomterminaldotshopterminalsdkgo 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "fmt" 9 | "net/http" 10 | "slices" 11 | 12 | "github.com/terminaldotshop/terminal-sdk-go/internal/apijson" 13 | "github.com/terminaldotshop/terminal-sdk-go/internal/param" 14 | "github.com/terminaldotshop/terminal-sdk-go/internal/requestconfig" 15 | "github.com/terminaldotshop/terminal-sdk-go/option" 16 | ) 17 | 18 | // AddressService contains methods and other services that help with interacting 19 | // with the terminal API. 20 | // 21 | // Note, unlike clients, this service does not read variables from the environment 22 | // automatically. You should not instantiate this service directly, and instead use 23 | // the [NewAddressService] method instead. 24 | type AddressService struct { 25 | Options []option.RequestOption 26 | } 27 | 28 | // NewAddressService generates a new service that applies the given options to each 29 | // request. These options are applied after the parent client's options (if there 30 | // is one), and before any request-specific options. 31 | func NewAddressService(opts ...option.RequestOption) (r *AddressService) { 32 | r = &AddressService{} 33 | r.Options = opts 34 | return 35 | } 36 | 37 | // Create and add a shipping address to the current user. 38 | func (r *AddressService) New(ctx context.Context, body AddressNewParams, opts ...option.RequestOption) (res *AddressNewResponse, err error) { 39 | opts = slices.Concat(r.Options, opts) 40 | path := "address" 41 | err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) 42 | return 43 | } 44 | 45 | // Get the shipping addresses associated with the current user. 46 | func (r *AddressService) List(ctx context.Context, opts ...option.RequestOption) (res *AddressListResponse, err error) { 47 | opts = slices.Concat(r.Options, opts) 48 | path := "address" 49 | err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) 50 | return 51 | } 52 | 53 | // Delete a shipping address from the current user. 54 | func (r *AddressService) Delete(ctx context.Context, id string, opts ...option.RequestOption) (res *AddressDeleteResponse, err error) { 55 | opts = slices.Concat(r.Options, opts) 56 | if id == "" { 57 | err = errors.New("missing required id parameter") 58 | return 59 | } 60 | path := fmt.Sprintf("address/%s", id) 61 | err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, &res, opts...) 62 | return 63 | } 64 | 65 | // Get the shipping address with the given ID. 66 | func (r *AddressService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *AddressGetResponse, err error) { 67 | opts = slices.Concat(r.Options, opts) 68 | if id == "" { 69 | err = errors.New("missing required id parameter") 70 | return 71 | } 72 | path := fmt.Sprintf("address/%s", id) 73 | err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) 74 | return 75 | } 76 | 77 | // Physical address associated with a Terminal shop user. 78 | type Address struct { 79 | // Unique object identifier. The format and length of IDs may change over time. 80 | ID string `json:"id,required"` 81 | // City of the address. 82 | City string `json:"city,required"` 83 | // ISO 3166-1 alpha-2 country code of the address. 84 | Country string `json:"country,required"` 85 | // Date the address was created. 86 | Created string `json:"created,required"` 87 | // The recipient's name. 88 | Name string `json:"name,required"` 89 | // Street of the address. 90 | Street1 string `json:"street1,required"` 91 | // Zip code of the address. 92 | Zip string `json:"zip,required"` 93 | // Phone number of the recipient. 94 | Phone string `json:"phone"` 95 | // Province or state of the address. 96 | Province string `json:"province"` 97 | // Apartment, suite, etc. of the address. 98 | Street2 string `json:"street2"` 99 | JSON addressJSON `json:"-"` 100 | } 101 | 102 | // addressJSON contains the JSON metadata for the struct [Address] 103 | type addressJSON struct { 104 | ID apijson.Field 105 | City apijson.Field 106 | Country apijson.Field 107 | Created apijson.Field 108 | Name apijson.Field 109 | Street1 apijson.Field 110 | Zip apijson.Field 111 | Phone apijson.Field 112 | Province apijson.Field 113 | Street2 apijson.Field 114 | raw string 115 | ExtraFields map[string]apijson.Field 116 | } 117 | 118 | func (r *Address) UnmarshalJSON(data []byte) (err error) { 119 | return apijson.UnmarshalRoot(data, r) 120 | } 121 | 122 | func (r addressJSON) RawJSON() string { 123 | return r.raw 124 | } 125 | 126 | type AddressNewResponse struct { 127 | // Shipping address ID. 128 | Data string `json:"data,required"` 129 | JSON addressNewResponseJSON `json:"-"` 130 | } 131 | 132 | // addressNewResponseJSON contains the JSON metadata for the struct 133 | // [AddressNewResponse] 134 | type addressNewResponseJSON struct { 135 | Data apijson.Field 136 | raw string 137 | ExtraFields map[string]apijson.Field 138 | } 139 | 140 | func (r *AddressNewResponse) UnmarshalJSON(data []byte) (err error) { 141 | return apijson.UnmarshalRoot(data, r) 142 | } 143 | 144 | func (r addressNewResponseJSON) RawJSON() string { 145 | return r.raw 146 | } 147 | 148 | type AddressListResponse struct { 149 | // Shipping addresses. 150 | Data []Address `json:"data,required"` 151 | JSON addressListResponseJSON `json:"-"` 152 | } 153 | 154 | // addressListResponseJSON contains the JSON metadata for the struct 155 | // [AddressListResponse] 156 | type addressListResponseJSON struct { 157 | Data apijson.Field 158 | raw string 159 | ExtraFields map[string]apijson.Field 160 | } 161 | 162 | func (r *AddressListResponse) UnmarshalJSON(data []byte) (err error) { 163 | return apijson.UnmarshalRoot(data, r) 164 | } 165 | 166 | func (r addressListResponseJSON) RawJSON() string { 167 | return r.raw 168 | } 169 | 170 | type AddressDeleteResponse struct { 171 | Data AddressDeleteResponseData `json:"data,required"` 172 | JSON addressDeleteResponseJSON `json:"-"` 173 | } 174 | 175 | // addressDeleteResponseJSON contains the JSON metadata for the struct 176 | // [AddressDeleteResponse] 177 | type addressDeleteResponseJSON struct { 178 | Data apijson.Field 179 | raw string 180 | ExtraFields map[string]apijson.Field 181 | } 182 | 183 | func (r *AddressDeleteResponse) UnmarshalJSON(data []byte) (err error) { 184 | return apijson.UnmarshalRoot(data, r) 185 | } 186 | 187 | func (r addressDeleteResponseJSON) RawJSON() string { 188 | return r.raw 189 | } 190 | 191 | type AddressDeleteResponseData string 192 | 193 | const ( 194 | AddressDeleteResponseDataOk AddressDeleteResponseData = "ok" 195 | ) 196 | 197 | func (r AddressDeleteResponseData) IsKnown() bool { 198 | switch r { 199 | case AddressDeleteResponseDataOk: 200 | return true 201 | } 202 | return false 203 | } 204 | 205 | type AddressGetResponse struct { 206 | // Physical address associated with a Terminal shop user. 207 | Data Address `json:"data,required"` 208 | JSON addressGetResponseJSON `json:"-"` 209 | } 210 | 211 | // addressGetResponseJSON contains the JSON metadata for the struct 212 | // [AddressGetResponse] 213 | type addressGetResponseJSON struct { 214 | Data apijson.Field 215 | raw string 216 | ExtraFields map[string]apijson.Field 217 | } 218 | 219 | func (r *AddressGetResponse) UnmarshalJSON(data []byte) (err error) { 220 | return apijson.UnmarshalRoot(data, r) 221 | } 222 | 223 | func (r addressGetResponseJSON) RawJSON() string { 224 | return r.raw 225 | } 226 | 227 | type AddressNewParams struct { 228 | // City of the address. 229 | City param.Field[string] `json:"city,required"` 230 | // ISO 3166-1 alpha-2 country code of the address. 231 | Country param.Field[string] `json:"country,required"` 232 | // The recipient's name. 233 | Name param.Field[string] `json:"name,required"` 234 | // Street of the address. 235 | Street1 param.Field[string] `json:"street1,required"` 236 | // Zip code of the address. 237 | Zip param.Field[string] `json:"zip,required"` 238 | // Phone number of the recipient. 239 | Phone param.Field[string] `json:"phone"` 240 | // Province or state of the address. 241 | Province param.Field[string] `json:"province"` 242 | // Apartment, suite, etc. of the address. 243 | Street2 param.Field[string] `json:"street2"` 244 | } 245 | 246 | func (r AddressNewParams) MarshalJSON() (data []byte, err error) { 247 | return apijson.MarshalRoot(r) 248 | } 249 | -------------------------------------------------------------------------------- /address_test.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package githubcomterminaldotshopterminalsdkgo_test 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "os" 9 | "testing" 10 | 11 | "github.com/terminaldotshop/terminal-sdk-go" 12 | "github.com/terminaldotshop/terminal-sdk-go/internal/testutil" 13 | "github.com/terminaldotshop/terminal-sdk-go/option" 14 | ) 15 | 16 | func TestAddressNewWithOptionalParams(t *testing.T) { 17 | baseURL := "http://localhost:4010" 18 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 19 | baseURL = envURL 20 | } 21 | if !testutil.CheckTestServer(t, baseURL) { 22 | return 23 | } 24 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 25 | option.WithBaseURL(baseURL), 26 | option.WithBearerToken("My Bearer Token"), 27 | ) 28 | _, err := client.Address.New(context.TODO(), githubcomterminaldotshopterminalsdkgo.AddressNewParams{ 29 | City: githubcomterminaldotshopterminalsdkgo.F("Anytown"), 30 | Country: githubcomterminaldotshopterminalsdkgo.F("US"), 31 | Name: githubcomterminaldotshopterminalsdkgo.F("John Doe"), 32 | Street1: githubcomterminaldotshopterminalsdkgo.F("123 Main St"), 33 | Zip: githubcomterminaldotshopterminalsdkgo.F("12345"), 34 | Phone: githubcomterminaldotshopterminalsdkgo.F("5555555555"), 35 | Province: githubcomterminaldotshopterminalsdkgo.F("CA"), 36 | Street2: githubcomterminaldotshopterminalsdkgo.F("Apt 1"), 37 | }) 38 | if err != nil { 39 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 40 | if errors.As(err, &apierr) { 41 | t.Log(string(apierr.DumpRequest(true))) 42 | } 43 | t.Fatalf("err should be nil: %s", err.Error()) 44 | } 45 | } 46 | 47 | func TestAddressList(t *testing.T) { 48 | baseURL := "http://localhost:4010" 49 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 50 | baseURL = envURL 51 | } 52 | if !testutil.CheckTestServer(t, baseURL) { 53 | return 54 | } 55 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 56 | option.WithBaseURL(baseURL), 57 | option.WithBearerToken("My Bearer Token"), 58 | ) 59 | _, err := client.Address.List(context.TODO()) 60 | if err != nil { 61 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 62 | if errors.As(err, &apierr) { 63 | t.Log(string(apierr.DumpRequest(true))) 64 | } 65 | t.Fatalf("err should be nil: %s", err.Error()) 66 | } 67 | } 68 | 69 | func TestAddressDelete(t *testing.T) { 70 | baseURL := "http://localhost:4010" 71 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 72 | baseURL = envURL 73 | } 74 | if !testutil.CheckTestServer(t, baseURL) { 75 | return 76 | } 77 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 78 | option.WithBaseURL(baseURL), 79 | option.WithBearerToken("My Bearer Token"), 80 | ) 81 | _, err := client.Address.Delete(context.TODO(), "shp_XXXXXXXXXXXXXXXXXXXXXXXXX") 82 | if err != nil { 83 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 84 | if errors.As(err, &apierr) { 85 | t.Log(string(apierr.DumpRequest(true))) 86 | } 87 | t.Fatalf("err should be nil: %s", err.Error()) 88 | } 89 | } 90 | 91 | func TestAddressGet(t *testing.T) { 92 | baseURL := "http://localhost:4010" 93 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 94 | baseURL = envURL 95 | } 96 | if !testutil.CheckTestServer(t, baseURL) { 97 | return 98 | } 99 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 100 | option.WithBaseURL(baseURL), 101 | option.WithBearerToken("My Bearer Token"), 102 | ) 103 | _, err := client.Address.Get(context.TODO(), "shp_XXXXXXXXXXXXXXXXXXXXXXXXX") 104 | if err != nil { 105 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 106 | if errors.As(err, &apierr) { 107 | t.Log(string(apierr.DumpRequest(true))) 108 | } 109 | t.Fatalf("err should be nil: %s", err.Error()) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /aliases.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package githubcomterminaldotshopterminalsdkgo 4 | 5 | import ( 6 | "github.com/terminaldotshop/terminal-sdk-go/internal/apierror" 7 | ) 8 | 9 | type Error = apierror.Error 10 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package githubcomterminaldotshopterminalsdkgo 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "fmt" 9 | "net/http" 10 | "slices" 11 | 12 | "github.com/terminaldotshop/terminal-sdk-go/internal/apijson" 13 | "github.com/terminaldotshop/terminal-sdk-go/internal/param" 14 | "github.com/terminaldotshop/terminal-sdk-go/internal/requestconfig" 15 | "github.com/terminaldotshop/terminal-sdk-go/option" 16 | ) 17 | 18 | // AppService contains methods and other services that help with interacting with 19 | // the terminal API. 20 | // 21 | // Note, unlike clients, this service does not read variables from the environment 22 | // automatically. You should not instantiate this service directly, and instead use 23 | // the [NewAppService] method instead. 24 | type AppService struct { 25 | Options []option.RequestOption 26 | } 27 | 28 | // NewAppService generates a new service that applies the given options to each 29 | // request. These options are applied after the parent client's options (if there 30 | // is one), and before any request-specific options. 31 | func NewAppService(opts ...option.RequestOption) (r *AppService) { 32 | r = &AppService{} 33 | r.Options = opts 34 | return 35 | } 36 | 37 | // Create an app. 38 | func (r *AppService) New(ctx context.Context, body AppNewParams, opts ...option.RequestOption) (res *AppNewResponse, err error) { 39 | opts = slices.Concat(r.Options, opts) 40 | path := "app" 41 | err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) 42 | return 43 | } 44 | 45 | // List the current user's registered apps. 46 | func (r *AppService) List(ctx context.Context, opts ...option.RequestOption) (res *AppListResponse, err error) { 47 | opts = slices.Concat(r.Options, opts) 48 | path := "app" 49 | err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) 50 | return 51 | } 52 | 53 | // Delete the app with the given ID. 54 | func (r *AppService) Delete(ctx context.Context, id string, opts ...option.RequestOption) (res *AppDeleteResponse, err error) { 55 | opts = slices.Concat(r.Options, opts) 56 | if id == "" { 57 | err = errors.New("missing required id parameter") 58 | return 59 | } 60 | path := fmt.Sprintf("app/%s", id) 61 | err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, &res, opts...) 62 | return 63 | } 64 | 65 | // Get the app with the given ID. 66 | func (r *AppService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *AppGetResponse, err error) { 67 | opts = slices.Concat(r.Options, opts) 68 | if id == "" { 69 | err = errors.New("missing required id parameter") 70 | return 71 | } 72 | path := fmt.Sprintf("app/%s", id) 73 | err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) 74 | return 75 | } 76 | 77 | // A Terminal App used for configuring an OAuth 2.0 client. 78 | type App struct { 79 | // Unique object identifier. The format and length of IDs may change over time. 80 | ID string `json:"id,required"` 81 | // Name of the app. 82 | Name string `json:"name,required"` 83 | // Redirect URI of the app. 84 | RedirectUri string `json:"redirectURI,required"` 85 | // OAuth 2.0 client secret of the app (obfuscated). 86 | Secret string `json:"secret,required"` 87 | JSON appJSON `json:"-"` 88 | } 89 | 90 | // appJSON contains the JSON metadata for the struct [App] 91 | type appJSON struct { 92 | ID apijson.Field 93 | Name apijson.Field 94 | RedirectUri apijson.Field 95 | Secret apijson.Field 96 | raw string 97 | ExtraFields map[string]apijson.Field 98 | } 99 | 100 | func (r *App) UnmarshalJSON(data []byte) (err error) { 101 | return apijson.UnmarshalRoot(data, r) 102 | } 103 | 104 | func (r appJSON) RawJSON() string { 105 | return r.raw 106 | } 107 | 108 | type AppNewResponse struct { 109 | Data AppNewResponseData `json:"data,required"` 110 | JSON appNewResponseJSON `json:"-"` 111 | } 112 | 113 | // appNewResponseJSON contains the JSON metadata for the struct [AppNewResponse] 114 | type appNewResponseJSON struct { 115 | Data apijson.Field 116 | raw string 117 | ExtraFields map[string]apijson.Field 118 | } 119 | 120 | func (r *AppNewResponse) UnmarshalJSON(data []byte) (err error) { 121 | return apijson.UnmarshalRoot(data, r) 122 | } 123 | 124 | func (r appNewResponseJSON) RawJSON() string { 125 | return r.raw 126 | } 127 | 128 | type AppNewResponseData struct { 129 | // OAuth 2.0 client ID. 130 | ID string `json:"id,required"` 131 | // OAuth 2.0 client secret. 132 | Secret string `json:"secret,required"` 133 | JSON appNewResponseDataJSON `json:"-"` 134 | } 135 | 136 | // appNewResponseDataJSON contains the JSON metadata for the struct 137 | // [AppNewResponseData] 138 | type appNewResponseDataJSON struct { 139 | ID apijson.Field 140 | Secret apijson.Field 141 | raw string 142 | ExtraFields map[string]apijson.Field 143 | } 144 | 145 | func (r *AppNewResponseData) UnmarshalJSON(data []byte) (err error) { 146 | return apijson.UnmarshalRoot(data, r) 147 | } 148 | 149 | func (r appNewResponseDataJSON) RawJSON() string { 150 | return r.raw 151 | } 152 | 153 | type AppListResponse struct { 154 | // List of apps. 155 | Data []App `json:"data,required"` 156 | JSON appListResponseJSON `json:"-"` 157 | } 158 | 159 | // appListResponseJSON contains the JSON metadata for the struct [AppListResponse] 160 | type appListResponseJSON struct { 161 | Data apijson.Field 162 | raw string 163 | ExtraFields map[string]apijson.Field 164 | } 165 | 166 | func (r *AppListResponse) UnmarshalJSON(data []byte) (err error) { 167 | return apijson.UnmarshalRoot(data, r) 168 | } 169 | 170 | func (r appListResponseJSON) RawJSON() string { 171 | return r.raw 172 | } 173 | 174 | type AppDeleteResponse struct { 175 | Data AppDeleteResponseData `json:"data,required"` 176 | JSON appDeleteResponseJSON `json:"-"` 177 | } 178 | 179 | // appDeleteResponseJSON contains the JSON metadata for the struct 180 | // [AppDeleteResponse] 181 | type appDeleteResponseJSON struct { 182 | Data apijson.Field 183 | raw string 184 | ExtraFields map[string]apijson.Field 185 | } 186 | 187 | func (r *AppDeleteResponse) UnmarshalJSON(data []byte) (err error) { 188 | return apijson.UnmarshalRoot(data, r) 189 | } 190 | 191 | func (r appDeleteResponseJSON) RawJSON() string { 192 | return r.raw 193 | } 194 | 195 | type AppDeleteResponseData string 196 | 197 | const ( 198 | AppDeleteResponseDataOk AppDeleteResponseData = "ok" 199 | ) 200 | 201 | func (r AppDeleteResponseData) IsKnown() bool { 202 | switch r { 203 | case AppDeleteResponseDataOk: 204 | return true 205 | } 206 | return false 207 | } 208 | 209 | type AppGetResponse struct { 210 | // A Terminal App used for configuring an OAuth 2.0 client. 211 | Data App `json:"data,required"` 212 | JSON appGetResponseJSON `json:"-"` 213 | } 214 | 215 | // appGetResponseJSON contains the JSON metadata for the struct [AppGetResponse] 216 | type appGetResponseJSON struct { 217 | Data apijson.Field 218 | raw string 219 | ExtraFields map[string]apijson.Field 220 | } 221 | 222 | func (r *AppGetResponse) UnmarshalJSON(data []byte) (err error) { 223 | return apijson.UnmarshalRoot(data, r) 224 | } 225 | 226 | func (r appGetResponseJSON) RawJSON() string { 227 | return r.raw 228 | } 229 | 230 | type AppNewParams struct { 231 | Name param.Field[string] `json:"name,required"` 232 | RedirectUri param.Field[string] `json:"redirectURI,required"` 233 | } 234 | 235 | func (r AppNewParams) MarshalJSON() (data []byte, err error) { 236 | return apijson.MarshalRoot(r) 237 | } 238 | -------------------------------------------------------------------------------- /app_test.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package githubcomterminaldotshopterminalsdkgo_test 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "os" 9 | "testing" 10 | 11 | "github.com/terminaldotshop/terminal-sdk-go" 12 | "github.com/terminaldotshop/terminal-sdk-go/internal/testutil" 13 | "github.com/terminaldotshop/terminal-sdk-go/option" 14 | ) 15 | 16 | func TestAppNew(t *testing.T) { 17 | baseURL := "http://localhost:4010" 18 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 19 | baseURL = envURL 20 | } 21 | if !testutil.CheckTestServer(t, baseURL) { 22 | return 23 | } 24 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 25 | option.WithBaseURL(baseURL), 26 | option.WithBearerToken("My Bearer Token"), 27 | ) 28 | _, err := client.App.New(context.TODO(), githubcomterminaldotshopterminalsdkgo.AppNewParams{ 29 | Name: githubcomterminaldotshopterminalsdkgo.F("Example App"), 30 | RedirectUri: githubcomterminaldotshopterminalsdkgo.F("https://example.com/callback"), 31 | }) 32 | if err != nil { 33 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 34 | if errors.As(err, &apierr) { 35 | t.Log(string(apierr.DumpRequest(true))) 36 | } 37 | t.Fatalf("err should be nil: %s", err.Error()) 38 | } 39 | } 40 | 41 | func TestAppList(t *testing.T) { 42 | baseURL := "http://localhost:4010" 43 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 44 | baseURL = envURL 45 | } 46 | if !testutil.CheckTestServer(t, baseURL) { 47 | return 48 | } 49 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 50 | option.WithBaseURL(baseURL), 51 | option.WithBearerToken("My Bearer Token"), 52 | ) 53 | _, err := client.App.List(context.TODO()) 54 | if err != nil { 55 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 56 | if errors.As(err, &apierr) { 57 | t.Log(string(apierr.DumpRequest(true))) 58 | } 59 | t.Fatalf("err should be nil: %s", err.Error()) 60 | } 61 | } 62 | 63 | func TestAppDelete(t *testing.T) { 64 | baseURL := "http://localhost:4010" 65 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 66 | baseURL = envURL 67 | } 68 | if !testutil.CheckTestServer(t, baseURL) { 69 | return 70 | } 71 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 72 | option.WithBaseURL(baseURL), 73 | option.WithBearerToken("My Bearer Token"), 74 | ) 75 | _, err := client.App.Delete(context.TODO(), "cli_XXXXXXXXXXXXXXXXXXXXXXXXX") 76 | if err != nil { 77 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 78 | if errors.As(err, &apierr) { 79 | t.Log(string(apierr.DumpRequest(true))) 80 | } 81 | t.Fatalf("err should be nil: %s", err.Error()) 82 | } 83 | } 84 | 85 | func TestAppGet(t *testing.T) { 86 | baseURL := "http://localhost:4010" 87 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 88 | baseURL = envURL 89 | } 90 | if !testutil.CheckTestServer(t, baseURL) { 91 | return 92 | } 93 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 94 | option.WithBaseURL(baseURL), 95 | option.WithBearerToken("My Bearer Token"), 96 | ) 97 | _, err := client.App.Get(context.TODO(), "cli_XXXXXXXXXXXXXXXXXXXXXXXXX") 98 | if err != nil { 99 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 100 | if errors.As(err, &apierr) { 101 | t.Log(string(apierr.DumpRequest(true))) 102 | } 103 | t.Fatalf("err should be nil: %s", err.Error()) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /card.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package githubcomterminaldotshopterminalsdkgo 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "fmt" 9 | "net/http" 10 | "slices" 11 | 12 | "github.com/terminaldotshop/terminal-sdk-go/internal/apijson" 13 | "github.com/terminaldotshop/terminal-sdk-go/internal/param" 14 | "github.com/terminaldotshop/terminal-sdk-go/internal/requestconfig" 15 | "github.com/terminaldotshop/terminal-sdk-go/option" 16 | ) 17 | 18 | // CardService contains methods and other services that help with interacting with 19 | // the terminal API. 20 | // 21 | // Note, unlike clients, this service does not read variables from the environment 22 | // automatically. You should not instantiate this service directly, and instead use 23 | // the [NewCardService] method instead. 24 | type CardService struct { 25 | Options []option.RequestOption 26 | } 27 | 28 | // NewCardService generates a new service that applies the given options to each 29 | // request. These options are applied after the parent client's options (if there 30 | // is one), and before any request-specific options. 31 | func NewCardService(opts ...option.RequestOption) (r *CardService) { 32 | r = &CardService{} 33 | r.Options = opts 34 | return 35 | } 36 | 37 | // Attach a credit card (tokenized via Stripe) to the current user. 38 | func (r *CardService) New(ctx context.Context, body CardNewParams, opts ...option.RequestOption) (res *CardNewResponse, err error) { 39 | opts = slices.Concat(r.Options, opts) 40 | path := "card" 41 | err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) 42 | return 43 | } 44 | 45 | // List the credit cards associated with the current user. 46 | func (r *CardService) List(ctx context.Context, opts ...option.RequestOption) (res *CardListResponse, err error) { 47 | opts = slices.Concat(r.Options, opts) 48 | path := "card" 49 | err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) 50 | return 51 | } 52 | 53 | // Delete a credit card associated with the current user. 54 | func (r *CardService) Delete(ctx context.Context, id string, opts ...option.RequestOption) (res *CardDeleteResponse, err error) { 55 | opts = slices.Concat(r.Options, opts) 56 | if id == "" { 57 | err = errors.New("missing required id parameter") 58 | return 59 | } 60 | path := fmt.Sprintf("card/%s", id) 61 | err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, &res, opts...) 62 | return 63 | } 64 | 65 | // Create a temporary URL for collecting credit card information for the current 66 | // user. 67 | func (r *CardService) Collect(ctx context.Context, opts ...option.RequestOption) (res *CardCollectResponse, err error) { 68 | opts = slices.Concat(r.Options, opts) 69 | path := "card/collect" 70 | err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...) 71 | return 72 | } 73 | 74 | // Get a credit card by ID associated with the current user. 75 | func (r *CardService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *CardGetResponse, err error) { 76 | opts = slices.Concat(r.Options, opts) 77 | if id == "" { 78 | err = errors.New("missing required id parameter") 79 | return 80 | } 81 | path := fmt.Sprintf("card/%s", id) 82 | err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) 83 | return 84 | } 85 | 86 | // Credit card used for payments in the Terminal shop. 87 | type Card struct { 88 | // Unique object identifier. The format and length of IDs may change over time. 89 | ID string `json:"id,required"` 90 | // Brand of the card. 91 | Brand string `json:"brand,required"` 92 | // Date the card was created. 93 | Created string `json:"created,required"` 94 | // Expiration of the card. 95 | Expiration CardExpiration `json:"expiration,required"` 96 | // Last four digits of the card. 97 | Last4 string `json:"last4,required"` 98 | JSON cardJSON `json:"-"` 99 | } 100 | 101 | // cardJSON contains the JSON metadata for the struct [Card] 102 | type cardJSON struct { 103 | ID apijson.Field 104 | Brand apijson.Field 105 | Created apijson.Field 106 | Expiration apijson.Field 107 | Last4 apijson.Field 108 | raw string 109 | ExtraFields map[string]apijson.Field 110 | } 111 | 112 | func (r *Card) UnmarshalJSON(data []byte) (err error) { 113 | return apijson.UnmarshalRoot(data, r) 114 | } 115 | 116 | func (r cardJSON) RawJSON() string { 117 | return r.raw 118 | } 119 | 120 | // Expiration of the card. 121 | type CardExpiration struct { 122 | // Expiration month of the card. 123 | Month int64 `json:"month,required"` 124 | // Expiration year of the card. 125 | Year int64 `json:"year,required"` 126 | JSON cardExpirationJSON `json:"-"` 127 | } 128 | 129 | // cardExpirationJSON contains the JSON metadata for the struct [CardExpiration] 130 | type cardExpirationJSON struct { 131 | Month apijson.Field 132 | Year apijson.Field 133 | raw string 134 | ExtraFields map[string]apijson.Field 135 | } 136 | 137 | func (r *CardExpiration) UnmarshalJSON(data []byte) (err error) { 138 | return apijson.UnmarshalRoot(data, r) 139 | } 140 | 141 | func (r cardExpirationJSON) RawJSON() string { 142 | return r.raw 143 | } 144 | 145 | type CardNewResponse struct { 146 | // ID of the card. 147 | Data string `json:"data,required"` 148 | JSON cardNewResponseJSON `json:"-"` 149 | } 150 | 151 | // cardNewResponseJSON contains the JSON metadata for the struct [CardNewResponse] 152 | type cardNewResponseJSON struct { 153 | Data apijson.Field 154 | raw string 155 | ExtraFields map[string]apijson.Field 156 | } 157 | 158 | func (r *CardNewResponse) UnmarshalJSON(data []byte) (err error) { 159 | return apijson.UnmarshalRoot(data, r) 160 | } 161 | 162 | func (r cardNewResponseJSON) RawJSON() string { 163 | return r.raw 164 | } 165 | 166 | type CardListResponse struct { 167 | // List of cards associated with the user. 168 | Data []Card `json:"data,required"` 169 | JSON cardListResponseJSON `json:"-"` 170 | } 171 | 172 | // cardListResponseJSON contains the JSON metadata for the struct 173 | // [CardListResponse] 174 | type cardListResponseJSON struct { 175 | Data apijson.Field 176 | raw string 177 | ExtraFields map[string]apijson.Field 178 | } 179 | 180 | func (r *CardListResponse) UnmarshalJSON(data []byte) (err error) { 181 | return apijson.UnmarshalRoot(data, r) 182 | } 183 | 184 | func (r cardListResponseJSON) RawJSON() string { 185 | return r.raw 186 | } 187 | 188 | type CardDeleteResponse struct { 189 | Data CardDeleteResponseData `json:"data,required"` 190 | JSON cardDeleteResponseJSON `json:"-"` 191 | } 192 | 193 | // cardDeleteResponseJSON contains the JSON metadata for the struct 194 | // [CardDeleteResponse] 195 | type cardDeleteResponseJSON struct { 196 | Data apijson.Field 197 | raw string 198 | ExtraFields map[string]apijson.Field 199 | } 200 | 201 | func (r *CardDeleteResponse) UnmarshalJSON(data []byte) (err error) { 202 | return apijson.UnmarshalRoot(data, r) 203 | } 204 | 205 | func (r cardDeleteResponseJSON) RawJSON() string { 206 | return r.raw 207 | } 208 | 209 | type CardDeleteResponseData string 210 | 211 | const ( 212 | CardDeleteResponseDataOk CardDeleteResponseData = "ok" 213 | ) 214 | 215 | func (r CardDeleteResponseData) IsKnown() bool { 216 | switch r { 217 | case CardDeleteResponseDataOk: 218 | return true 219 | } 220 | return false 221 | } 222 | 223 | type CardCollectResponse struct { 224 | // URL for collecting card information. 225 | Data CardCollectResponseData `json:"data,required"` 226 | JSON cardCollectResponseJSON `json:"-"` 227 | } 228 | 229 | // cardCollectResponseJSON contains the JSON metadata for the struct 230 | // [CardCollectResponse] 231 | type cardCollectResponseJSON struct { 232 | Data apijson.Field 233 | raw string 234 | ExtraFields map[string]apijson.Field 235 | } 236 | 237 | func (r *CardCollectResponse) UnmarshalJSON(data []byte) (err error) { 238 | return apijson.UnmarshalRoot(data, r) 239 | } 240 | 241 | func (r cardCollectResponseJSON) RawJSON() string { 242 | return r.raw 243 | } 244 | 245 | // URL for collecting card information. 246 | type CardCollectResponseData struct { 247 | // Temporary URL that allows a user to enter credit card details over https at 248 | // terminal.shop. 249 | URL string `json:"url,required" format:"uri"` 250 | JSON cardCollectResponseDataJSON `json:"-"` 251 | } 252 | 253 | // cardCollectResponseDataJSON contains the JSON metadata for the struct 254 | // [CardCollectResponseData] 255 | type cardCollectResponseDataJSON struct { 256 | URL apijson.Field 257 | raw string 258 | ExtraFields map[string]apijson.Field 259 | } 260 | 261 | func (r *CardCollectResponseData) UnmarshalJSON(data []byte) (err error) { 262 | return apijson.UnmarshalRoot(data, r) 263 | } 264 | 265 | func (r cardCollectResponseDataJSON) RawJSON() string { 266 | return r.raw 267 | } 268 | 269 | type CardGetResponse struct { 270 | // Credit card used for payments in the Terminal shop. 271 | Data Card `json:"data,required"` 272 | JSON cardGetResponseJSON `json:"-"` 273 | } 274 | 275 | // cardGetResponseJSON contains the JSON metadata for the struct [CardGetResponse] 276 | type cardGetResponseJSON struct { 277 | Data apijson.Field 278 | raw string 279 | ExtraFields map[string]apijson.Field 280 | } 281 | 282 | func (r *CardGetResponse) UnmarshalJSON(data []byte) (err error) { 283 | return apijson.UnmarshalRoot(data, r) 284 | } 285 | 286 | func (r cardGetResponseJSON) RawJSON() string { 287 | return r.raw 288 | } 289 | 290 | type CardNewParams struct { 291 | // Stripe card token. Learn how to 292 | // [create one here](https://docs.stripe.com/api/tokens/create_card). 293 | Token param.Field[string] `json:"token,required"` 294 | } 295 | 296 | func (r CardNewParams) MarshalJSON() (data []byte, err error) { 297 | return apijson.MarshalRoot(r) 298 | } 299 | -------------------------------------------------------------------------------- /card_test.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package githubcomterminaldotshopterminalsdkgo_test 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "os" 9 | "testing" 10 | 11 | "github.com/terminaldotshop/terminal-sdk-go" 12 | "github.com/terminaldotshop/terminal-sdk-go/internal/testutil" 13 | "github.com/terminaldotshop/terminal-sdk-go/option" 14 | ) 15 | 16 | func TestCardNew(t *testing.T) { 17 | baseURL := "http://localhost:4010" 18 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 19 | baseURL = envURL 20 | } 21 | if !testutil.CheckTestServer(t, baseURL) { 22 | return 23 | } 24 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 25 | option.WithBaseURL(baseURL), 26 | option.WithBearerToken("My Bearer Token"), 27 | ) 28 | _, err := client.Card.New(context.TODO(), githubcomterminaldotshopterminalsdkgo.CardNewParams{ 29 | Token: githubcomterminaldotshopterminalsdkgo.F("tok_1N3T00LkdIwHu7ixt44h1F8k"), 30 | }) 31 | if err != nil { 32 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 33 | if errors.As(err, &apierr) { 34 | t.Log(string(apierr.DumpRequest(true))) 35 | } 36 | t.Fatalf("err should be nil: %s", err.Error()) 37 | } 38 | } 39 | 40 | func TestCardList(t *testing.T) { 41 | baseURL := "http://localhost:4010" 42 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 43 | baseURL = envURL 44 | } 45 | if !testutil.CheckTestServer(t, baseURL) { 46 | return 47 | } 48 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 49 | option.WithBaseURL(baseURL), 50 | option.WithBearerToken("My Bearer Token"), 51 | ) 52 | _, err := client.Card.List(context.TODO()) 53 | if err != nil { 54 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 55 | if errors.As(err, &apierr) { 56 | t.Log(string(apierr.DumpRequest(true))) 57 | } 58 | t.Fatalf("err should be nil: %s", err.Error()) 59 | } 60 | } 61 | 62 | func TestCardDelete(t *testing.T) { 63 | baseURL := "http://localhost:4010" 64 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 65 | baseURL = envURL 66 | } 67 | if !testutil.CheckTestServer(t, baseURL) { 68 | return 69 | } 70 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 71 | option.WithBaseURL(baseURL), 72 | option.WithBearerToken("My Bearer Token"), 73 | ) 74 | _, err := client.Card.Delete(context.TODO(), "crd_XXXXXXXXXXXXXXXXXXXXXXXXX") 75 | if err != nil { 76 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 77 | if errors.As(err, &apierr) { 78 | t.Log(string(apierr.DumpRequest(true))) 79 | } 80 | t.Fatalf("err should be nil: %s", err.Error()) 81 | } 82 | } 83 | 84 | func TestCardCollect(t *testing.T) { 85 | baseURL := "http://localhost:4010" 86 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 87 | baseURL = envURL 88 | } 89 | if !testutil.CheckTestServer(t, baseURL) { 90 | return 91 | } 92 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 93 | option.WithBaseURL(baseURL), 94 | option.WithBearerToken("My Bearer Token"), 95 | ) 96 | _, err := client.Card.Collect(context.TODO()) 97 | if err != nil { 98 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 99 | if errors.As(err, &apierr) { 100 | t.Log(string(apierr.DumpRequest(true))) 101 | } 102 | t.Fatalf("err should be nil: %s", err.Error()) 103 | } 104 | } 105 | 106 | func TestCardGet(t *testing.T) { 107 | baseURL := "http://localhost:4010" 108 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 109 | baseURL = envURL 110 | } 111 | if !testutil.CheckTestServer(t, baseURL) { 112 | return 113 | } 114 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 115 | option.WithBaseURL(baseURL), 116 | option.WithBearerToken("My Bearer Token"), 117 | ) 118 | _, err := client.Card.Get(context.TODO(), "crd_XXXXXXXXXXXXXXXXXXXXXXXXX") 119 | if err != nil { 120 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 121 | if errors.As(err, &apierr) { 122 | t.Log(string(apierr.DumpRequest(true))) 123 | } 124 | t.Fatalf("err should be nil: %s", err.Error()) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /cart_test.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package githubcomterminaldotshopterminalsdkgo_test 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "os" 9 | "testing" 10 | 11 | "github.com/terminaldotshop/terminal-sdk-go" 12 | "github.com/terminaldotshop/terminal-sdk-go/internal/testutil" 13 | "github.com/terminaldotshop/terminal-sdk-go/option" 14 | ) 15 | 16 | func TestCartClear(t *testing.T) { 17 | baseURL := "http://localhost:4010" 18 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 19 | baseURL = envURL 20 | } 21 | if !testutil.CheckTestServer(t, baseURL) { 22 | return 23 | } 24 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 25 | option.WithBaseURL(baseURL), 26 | option.WithBearerToken("My Bearer Token"), 27 | ) 28 | _, err := client.Cart.Clear(context.TODO()) 29 | if err != nil { 30 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 31 | if errors.As(err, &apierr) { 32 | t.Log(string(apierr.DumpRequest(true))) 33 | } 34 | t.Fatalf("err should be nil: %s", err.Error()) 35 | } 36 | } 37 | 38 | func TestCartConvert(t *testing.T) { 39 | baseURL := "http://localhost:4010" 40 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 41 | baseURL = envURL 42 | } 43 | if !testutil.CheckTestServer(t, baseURL) { 44 | return 45 | } 46 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 47 | option.WithBaseURL(baseURL), 48 | option.WithBearerToken("My Bearer Token"), 49 | ) 50 | _, err := client.Cart.Convert(context.TODO()) 51 | if err != nil { 52 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 53 | if errors.As(err, &apierr) { 54 | t.Log(string(apierr.DumpRequest(true))) 55 | } 56 | t.Fatalf("err should be nil: %s", err.Error()) 57 | } 58 | } 59 | 60 | func TestCartGet(t *testing.T) { 61 | baseURL := "http://localhost:4010" 62 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 63 | baseURL = envURL 64 | } 65 | if !testutil.CheckTestServer(t, baseURL) { 66 | return 67 | } 68 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 69 | option.WithBaseURL(baseURL), 70 | option.WithBearerToken("My Bearer Token"), 71 | ) 72 | _, err := client.Cart.Get(context.TODO()) 73 | if err != nil { 74 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 75 | if errors.As(err, &apierr) { 76 | t.Log(string(apierr.DumpRequest(true))) 77 | } 78 | t.Fatalf("err should be nil: %s", err.Error()) 79 | } 80 | } 81 | 82 | func TestCartSetAddress(t *testing.T) { 83 | baseURL := "http://localhost:4010" 84 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 85 | baseURL = envURL 86 | } 87 | if !testutil.CheckTestServer(t, baseURL) { 88 | return 89 | } 90 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 91 | option.WithBaseURL(baseURL), 92 | option.WithBearerToken("My Bearer Token"), 93 | ) 94 | _, err := client.Cart.SetAddress(context.TODO(), githubcomterminaldotshopterminalsdkgo.CartSetAddressParams{ 95 | AddressID: githubcomterminaldotshopterminalsdkgo.F("shp_XXXXXXXXXXXXXXXXXXXXXXXXX"), 96 | }) 97 | if err != nil { 98 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 99 | if errors.As(err, &apierr) { 100 | t.Log(string(apierr.DumpRequest(true))) 101 | } 102 | t.Fatalf("err should be nil: %s", err.Error()) 103 | } 104 | } 105 | 106 | func TestCartSetCard(t *testing.T) { 107 | baseURL := "http://localhost:4010" 108 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 109 | baseURL = envURL 110 | } 111 | if !testutil.CheckTestServer(t, baseURL) { 112 | return 113 | } 114 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 115 | option.WithBaseURL(baseURL), 116 | option.WithBearerToken("My Bearer Token"), 117 | ) 118 | _, err := client.Cart.SetCard(context.TODO(), githubcomterminaldotshopterminalsdkgo.CartSetCardParams{ 119 | CardID: githubcomterminaldotshopterminalsdkgo.F("crd_XXXXXXXXXXXXXXXXXXXXXXXXX"), 120 | }) 121 | if err != nil { 122 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 123 | if errors.As(err, &apierr) { 124 | t.Log(string(apierr.DumpRequest(true))) 125 | } 126 | t.Fatalf("err should be nil: %s", err.Error()) 127 | } 128 | } 129 | 130 | func TestCartSetItem(t *testing.T) { 131 | baseURL := "http://localhost:4010" 132 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 133 | baseURL = envURL 134 | } 135 | if !testutil.CheckTestServer(t, baseURL) { 136 | return 137 | } 138 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 139 | option.WithBaseURL(baseURL), 140 | option.WithBearerToken("My Bearer Token"), 141 | ) 142 | _, err := client.Cart.SetItem(context.TODO(), githubcomterminaldotshopterminalsdkgo.CartSetItemParams{ 143 | ProductVariantID: githubcomterminaldotshopterminalsdkgo.F("var_XXXXXXXXXXXXXXXXXXXXXXXXX"), 144 | Quantity: githubcomterminaldotshopterminalsdkgo.F(int64(2)), 145 | }) 146 | if err != nil { 147 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 148 | if errors.As(err, &apierr) { 149 | t.Log(string(apierr.DumpRequest(true))) 150 | } 151 | t.Fatalf("err should be nil: %s", err.Error()) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package githubcomterminaldotshopterminalsdkgo 4 | 5 | import ( 6 | "context" 7 | "net/http" 8 | "os" 9 | "slices" 10 | 11 | "github.com/terminaldotshop/terminal-sdk-go/internal/requestconfig" 12 | "github.com/terminaldotshop/terminal-sdk-go/option" 13 | ) 14 | 15 | // Client creates a struct with services and top level methods that help with 16 | // interacting with the terminal API. You should not instantiate this client 17 | // directly, and instead use the [NewClient] method instead. 18 | type Client struct { 19 | Options []option.RequestOption 20 | Product *ProductService 21 | Profile *ProfileService 22 | Address *AddressService 23 | Card *CardService 24 | Cart *CartService 25 | Order *OrderService 26 | Subscription *SubscriptionService 27 | Token *TokenService 28 | App *AppService 29 | Email *EmailService 30 | View *ViewService 31 | } 32 | 33 | // DefaultClientOptions read from the environment (TERMINAL_BEARER_TOKEN, 34 | // TERMINAL_BASE_URL). This should be used to initialize new clients. 35 | func DefaultClientOptions() []option.RequestOption { 36 | defaults := []option.RequestOption{option.WithEnvironmentProduction()} 37 | if o, ok := os.LookupEnv("TERMINAL_BASE_URL"); ok { 38 | defaults = append(defaults, option.WithBaseURL(o)) 39 | } 40 | if o, ok := os.LookupEnv("TERMINAL_BEARER_TOKEN"); ok { 41 | defaults = append(defaults, option.WithBearerToken(o)) 42 | } 43 | return defaults 44 | } 45 | 46 | // NewClient generates a new client with the default option read from the 47 | // environment (TERMINAL_BEARER_TOKEN, TERMINAL_BASE_URL). The option passed in as 48 | // arguments are applied after these default arguments, and all option will be 49 | // passed down to the services and requests that this client makes. 50 | func NewClient(opts ...option.RequestOption) (r *Client) { 51 | opts = append(DefaultClientOptions(), opts...) 52 | 53 | r = &Client{Options: opts} 54 | 55 | r.Product = NewProductService(opts...) 56 | r.Profile = NewProfileService(opts...) 57 | r.Address = NewAddressService(opts...) 58 | r.Card = NewCardService(opts...) 59 | r.Cart = NewCartService(opts...) 60 | r.Order = NewOrderService(opts...) 61 | r.Subscription = NewSubscriptionService(opts...) 62 | r.Token = NewTokenService(opts...) 63 | r.App = NewAppService(opts...) 64 | r.Email = NewEmailService(opts...) 65 | r.View = NewViewService(opts...) 66 | 67 | return 68 | } 69 | 70 | // Execute makes a request with the given context, method, URL, request params, 71 | // response, and request options. This is useful for hitting undocumented endpoints 72 | // while retaining the base URL, auth, retries, and other options from the client. 73 | // 74 | // If a byte slice or an [io.Reader] is supplied to params, it will be used as-is 75 | // for the request body. 76 | // 77 | // The params is by default serialized into the body using [encoding/json]. If your 78 | // type implements a MarshalJSON function, it will be used instead to serialize the 79 | // request. If a URLQuery method is implemented, the returned [url.Values] will be 80 | // used as query strings to the url. 81 | // 82 | // If your params struct uses [param.Field], you must provide either [MarshalJSON], 83 | // [URLQuery], and/or [MarshalForm] functions. It is undefined behavior to use a 84 | // struct uses [param.Field] without specifying how it is serialized. 85 | // 86 | // Any "…Params" object defined in this library can be used as the request 87 | // argument. Note that 'path' arguments will not be forwarded into the url. 88 | // 89 | // The response body will be deserialized into the res variable, depending on its 90 | // type: 91 | // 92 | // - A pointer to a [*http.Response] is populated by the raw response. 93 | // - A pointer to a byte array will be populated with the contents of the request 94 | // body. 95 | // - A pointer to any other type uses this library's default JSON decoding, which 96 | // respects UnmarshalJSON if it is defined on the type. 97 | // - A nil value will not read the response body. 98 | // 99 | // For even greater flexibility, see [option.WithResponseInto] and 100 | // [option.WithResponseBodyInto]. 101 | func (r *Client) Execute(ctx context.Context, method string, path string, params interface{}, res interface{}, opts ...option.RequestOption) error { 102 | opts = slices.Concat(r.Options, opts) 103 | return requestconfig.ExecuteNewRequest(ctx, method, path, params, res, opts...) 104 | } 105 | 106 | // Get makes a GET request with the given URL, params, and optionally deserializes 107 | // to a response. See [Execute] documentation on the params and response. 108 | func (r *Client) Get(ctx context.Context, path string, params interface{}, res interface{}, opts ...option.RequestOption) error { 109 | return r.Execute(ctx, http.MethodGet, path, params, res, opts...) 110 | } 111 | 112 | // Post makes a POST request with the given URL, params, and optionally 113 | // deserializes to a response. See [Execute] documentation on the params and 114 | // response. 115 | func (r *Client) Post(ctx context.Context, path string, params interface{}, res interface{}, opts ...option.RequestOption) error { 116 | return r.Execute(ctx, http.MethodPost, path, params, res, opts...) 117 | } 118 | 119 | // Put makes a PUT request with the given URL, params, and optionally deserializes 120 | // to a response. See [Execute] documentation on the params and response. 121 | func (r *Client) Put(ctx context.Context, path string, params interface{}, res interface{}, opts ...option.RequestOption) error { 122 | return r.Execute(ctx, http.MethodPut, path, params, res, opts...) 123 | } 124 | 125 | // Patch makes a PATCH request with the given URL, params, and optionally 126 | // deserializes to a response. See [Execute] documentation on the params and 127 | // response. 128 | func (r *Client) Patch(ctx context.Context, path string, params interface{}, res interface{}, opts ...option.RequestOption) error { 129 | return r.Execute(ctx, http.MethodPatch, path, params, res, opts...) 130 | } 131 | 132 | // Delete makes a DELETE request with the given URL, params, and optionally 133 | // deserializes to a response. See [Execute] documentation on the params and 134 | // response. 135 | func (r *Client) Delete(ctx context.Context, path string, params interface{}, res interface{}, opts ...option.RequestOption) error { 136 | return r.Execute(ctx, http.MethodDelete, path, params, res, opts...) 137 | } 138 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package githubcomterminaldotshopterminalsdkgo_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "net/http" 9 | "reflect" 10 | "testing" 11 | "time" 12 | 13 | "github.com/terminaldotshop/terminal-sdk-go" 14 | "github.com/terminaldotshop/terminal-sdk-go/internal" 15 | "github.com/terminaldotshop/terminal-sdk-go/option" 16 | ) 17 | 18 | type closureTransport struct { 19 | fn func(req *http.Request) (*http.Response, error) 20 | } 21 | 22 | func (t *closureTransport) RoundTrip(req *http.Request) (*http.Response, error) { 23 | return t.fn(req) 24 | } 25 | 26 | func TestUserAgentHeader(t *testing.T) { 27 | var userAgent string 28 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 29 | option.WithBearerToken("My Bearer Token"), 30 | option.WithHTTPClient(&http.Client{ 31 | Transport: &closureTransport{ 32 | fn: func(req *http.Request) (*http.Response, error) { 33 | userAgent = req.Header.Get("User-Agent") 34 | return &http.Response{ 35 | StatusCode: http.StatusOK, 36 | }, nil 37 | }, 38 | }, 39 | }), 40 | ) 41 | client.Product.List(context.Background()) 42 | if userAgent != fmt.Sprintf("Terminal/Go %s", internal.PackageVersion) { 43 | t.Errorf("Expected User-Agent to be correct, but got: %#v", userAgent) 44 | } 45 | } 46 | 47 | func TestRetryAfter(t *testing.T) { 48 | retryCountHeaders := make([]string, 0) 49 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 50 | option.WithBearerToken("My Bearer Token"), 51 | option.WithHTTPClient(&http.Client{ 52 | Transport: &closureTransport{ 53 | fn: func(req *http.Request) (*http.Response, error) { 54 | retryCountHeaders = append(retryCountHeaders, req.Header.Get("X-Stainless-Retry-Count")) 55 | return &http.Response{ 56 | StatusCode: http.StatusTooManyRequests, 57 | Header: http.Header{ 58 | http.CanonicalHeaderKey("Retry-After"): []string{"0.1"}, 59 | }, 60 | }, nil 61 | }, 62 | }, 63 | }), 64 | ) 65 | _, err := client.Product.List(context.Background()) 66 | if err == nil { 67 | t.Error("Expected there to be a cancel error") 68 | } 69 | 70 | attempts := len(retryCountHeaders) 71 | if attempts != 3 { 72 | t.Errorf("Expected %d attempts, got %d", 3, attempts) 73 | } 74 | 75 | expectedRetryCountHeaders := []string{"0", "1", "2"} 76 | if !reflect.DeepEqual(retryCountHeaders, expectedRetryCountHeaders) { 77 | t.Errorf("Expected %v retry count headers, got %v", expectedRetryCountHeaders, retryCountHeaders) 78 | } 79 | } 80 | 81 | func TestDeleteRetryCountHeader(t *testing.T) { 82 | retryCountHeaders := make([]string, 0) 83 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 84 | option.WithBearerToken("My Bearer Token"), 85 | option.WithHTTPClient(&http.Client{ 86 | Transport: &closureTransport{ 87 | fn: func(req *http.Request) (*http.Response, error) { 88 | retryCountHeaders = append(retryCountHeaders, req.Header.Get("X-Stainless-Retry-Count")) 89 | return &http.Response{ 90 | StatusCode: http.StatusTooManyRequests, 91 | Header: http.Header{ 92 | http.CanonicalHeaderKey("Retry-After"): []string{"0.1"}, 93 | }, 94 | }, nil 95 | }, 96 | }, 97 | }), 98 | option.WithHeaderDel("X-Stainless-Retry-Count"), 99 | ) 100 | _, err := client.Product.List(context.Background()) 101 | if err == nil { 102 | t.Error("Expected there to be a cancel error") 103 | } 104 | 105 | expectedRetryCountHeaders := []string{"", "", ""} 106 | if !reflect.DeepEqual(retryCountHeaders, expectedRetryCountHeaders) { 107 | t.Errorf("Expected %v retry count headers, got %v", expectedRetryCountHeaders, retryCountHeaders) 108 | } 109 | } 110 | 111 | func TestOverwriteRetryCountHeader(t *testing.T) { 112 | retryCountHeaders := make([]string, 0) 113 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 114 | option.WithBearerToken("My Bearer Token"), 115 | option.WithHTTPClient(&http.Client{ 116 | Transport: &closureTransport{ 117 | fn: func(req *http.Request) (*http.Response, error) { 118 | retryCountHeaders = append(retryCountHeaders, req.Header.Get("X-Stainless-Retry-Count")) 119 | return &http.Response{ 120 | StatusCode: http.StatusTooManyRequests, 121 | Header: http.Header{ 122 | http.CanonicalHeaderKey("Retry-After"): []string{"0.1"}, 123 | }, 124 | }, nil 125 | }, 126 | }, 127 | }), 128 | option.WithHeader("X-Stainless-Retry-Count", "42"), 129 | ) 130 | _, err := client.Product.List(context.Background()) 131 | if err == nil { 132 | t.Error("Expected there to be a cancel error") 133 | } 134 | 135 | expectedRetryCountHeaders := []string{"42", "42", "42"} 136 | if !reflect.DeepEqual(retryCountHeaders, expectedRetryCountHeaders) { 137 | t.Errorf("Expected %v retry count headers, got %v", expectedRetryCountHeaders, retryCountHeaders) 138 | } 139 | } 140 | 141 | func TestRetryAfterMs(t *testing.T) { 142 | attempts := 0 143 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 144 | option.WithBearerToken("My Bearer Token"), 145 | option.WithHTTPClient(&http.Client{ 146 | Transport: &closureTransport{ 147 | fn: func(req *http.Request) (*http.Response, error) { 148 | attempts++ 149 | return &http.Response{ 150 | StatusCode: http.StatusTooManyRequests, 151 | Header: http.Header{ 152 | http.CanonicalHeaderKey("Retry-After-Ms"): []string{"100"}, 153 | }, 154 | }, nil 155 | }, 156 | }, 157 | }), 158 | ) 159 | _, err := client.Product.List(context.Background()) 160 | if err == nil { 161 | t.Error("Expected there to be a cancel error") 162 | } 163 | if want := 3; attempts != want { 164 | t.Errorf("Expected %d attempts, got %d", want, attempts) 165 | } 166 | } 167 | 168 | func TestContextCancel(t *testing.T) { 169 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 170 | option.WithBearerToken("My Bearer Token"), 171 | option.WithHTTPClient(&http.Client{ 172 | Transport: &closureTransport{ 173 | fn: func(req *http.Request) (*http.Response, error) { 174 | <-req.Context().Done() 175 | return nil, req.Context().Err() 176 | }, 177 | }, 178 | }), 179 | ) 180 | cancelCtx, cancel := context.WithCancel(context.Background()) 181 | cancel() 182 | _, err := client.Product.List(cancelCtx) 183 | if err == nil { 184 | t.Error("Expected there to be a cancel error") 185 | } 186 | } 187 | 188 | func TestContextCancelDelay(t *testing.T) { 189 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 190 | option.WithBearerToken("My Bearer Token"), 191 | option.WithHTTPClient(&http.Client{ 192 | Transport: &closureTransport{ 193 | fn: func(req *http.Request) (*http.Response, error) { 194 | <-req.Context().Done() 195 | return nil, req.Context().Err() 196 | }, 197 | }, 198 | }), 199 | ) 200 | cancelCtx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond) 201 | defer cancel() 202 | _, err := client.Product.List(cancelCtx) 203 | if err == nil { 204 | t.Error("expected there to be a cancel error") 205 | } 206 | } 207 | 208 | func TestContextDeadline(t *testing.T) { 209 | testTimeout := time.After(3 * time.Second) 210 | testDone := make(chan struct{}) 211 | 212 | deadline := time.Now().Add(100 * time.Millisecond) 213 | deadlineCtx, cancel := context.WithDeadline(context.Background(), deadline) 214 | defer cancel() 215 | 216 | go func() { 217 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 218 | option.WithBearerToken("My Bearer Token"), 219 | option.WithHTTPClient(&http.Client{ 220 | Transport: &closureTransport{ 221 | fn: func(req *http.Request) (*http.Response, error) { 222 | <-req.Context().Done() 223 | return nil, req.Context().Err() 224 | }, 225 | }, 226 | }), 227 | ) 228 | _, err := client.Product.List(deadlineCtx) 229 | if err == nil { 230 | t.Error("expected there to be a deadline error") 231 | } 232 | close(testDone) 233 | }() 234 | 235 | select { 236 | case <-testTimeout: 237 | t.Fatal("client didn't finish in time") 238 | case <-testDone: 239 | if diff := time.Since(deadline); diff < -30*time.Millisecond || 30*time.Millisecond < diff { 240 | t.Fatalf("client did not return within 30ms of context deadline, got %s", diff) 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /email.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package githubcomterminaldotshopterminalsdkgo 4 | 5 | import ( 6 | "context" 7 | "net/http" 8 | "slices" 9 | 10 | "github.com/terminaldotshop/terminal-sdk-go/internal/apijson" 11 | "github.com/terminaldotshop/terminal-sdk-go/internal/param" 12 | "github.com/terminaldotshop/terminal-sdk-go/internal/requestconfig" 13 | "github.com/terminaldotshop/terminal-sdk-go/option" 14 | ) 15 | 16 | // EmailService contains methods and other services that help with interacting with 17 | // the terminal API. 18 | // 19 | // Note, unlike clients, this service does not read variables from the environment 20 | // automatically. You should not instantiate this service directly, and instead use 21 | // the [NewEmailService] method instead. 22 | type EmailService struct { 23 | Options []option.RequestOption 24 | } 25 | 26 | // NewEmailService generates a new service that applies the given options to each 27 | // request. These options are applied after the parent client's options (if there 28 | // is one), and before any request-specific options. 29 | func NewEmailService(opts ...option.RequestOption) (r *EmailService) { 30 | r = &EmailService{} 31 | r.Options = opts 32 | return 33 | } 34 | 35 | // Subscribe to email updates from Terminal. 36 | func (r *EmailService) New(ctx context.Context, body EmailNewParams, opts ...option.RequestOption) (res *EmailNewResponse, err error) { 37 | opts = slices.Concat(r.Options, opts) 38 | path := "email" 39 | err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) 40 | return 41 | } 42 | 43 | type EmailNewResponse struct { 44 | Data EmailNewResponseData `json:"data,required"` 45 | JSON emailNewResponseJSON `json:"-"` 46 | } 47 | 48 | // emailNewResponseJSON contains the JSON metadata for the struct 49 | // [EmailNewResponse] 50 | type emailNewResponseJSON struct { 51 | Data apijson.Field 52 | raw string 53 | ExtraFields map[string]apijson.Field 54 | } 55 | 56 | func (r *EmailNewResponse) UnmarshalJSON(data []byte) (err error) { 57 | return apijson.UnmarshalRoot(data, r) 58 | } 59 | 60 | func (r emailNewResponseJSON) RawJSON() string { 61 | return r.raw 62 | } 63 | 64 | type EmailNewResponseData string 65 | 66 | const ( 67 | EmailNewResponseDataOk EmailNewResponseData = "ok" 68 | ) 69 | 70 | func (r EmailNewResponseData) IsKnown() bool { 71 | switch r { 72 | case EmailNewResponseDataOk: 73 | return true 74 | } 75 | return false 76 | } 77 | 78 | type EmailNewParams struct { 79 | // Email address to subscribe to Terminal updates with. 80 | Email param.Field[string] `json:"email,required" format:"email"` 81 | } 82 | 83 | func (r EmailNewParams) MarshalJSON() (data []byte, err error) { 84 | return apijson.MarshalRoot(r) 85 | } 86 | -------------------------------------------------------------------------------- /email_test.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package githubcomterminaldotshopterminalsdkgo_test 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "os" 9 | "testing" 10 | 11 | "github.com/terminaldotshop/terminal-sdk-go" 12 | "github.com/terminaldotshop/terminal-sdk-go/internal/testutil" 13 | "github.com/terminaldotshop/terminal-sdk-go/option" 14 | ) 15 | 16 | func TestEmailNew(t *testing.T) { 17 | baseURL := "http://localhost:4010" 18 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 19 | baseURL = envURL 20 | } 21 | if !testutil.CheckTestServer(t, baseURL) { 22 | return 23 | } 24 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 25 | option.WithBaseURL(baseURL), 26 | option.WithBearerToken("My Bearer Token"), 27 | ) 28 | _, err := client.Email.New(context.TODO(), githubcomterminaldotshopterminalsdkgo.EmailNewParams{ 29 | Email: githubcomterminaldotshopterminalsdkgo.F("john@example.com"), 30 | }) 31 | if err != nil { 32 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 33 | if errors.As(err, &apierr) { 34 | t.Log(string(apierr.DumpRequest(true))) 35 | } 36 | t.Fatalf("err should be nil: %s", err.Error()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/.keep: -------------------------------------------------------------------------------- 1 | File generated from our OpenAPI spec by Stainless. 2 | 3 | This directory can be used to store example files demonstrating usage of this SDK. 4 | It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. -------------------------------------------------------------------------------- /field.go: -------------------------------------------------------------------------------- 1 | package githubcomterminaldotshopterminalsdkgo 2 | 3 | import ( 4 | "github.com/terminaldotshop/terminal-sdk-go/internal/param" 5 | "io" 6 | ) 7 | 8 | // F is a param field helper used to initialize a [param.Field] generic struct. 9 | // This helps specify null, zero values, and overrides, as well as normal values. 10 | // You can read more about this in our [README]. 11 | // 12 | // [README]: https://pkg.go.dev/github.com/terminaldotshop/terminal-sdk-go#readme-request-fields 13 | func F[T any](value T) param.Field[T] { return param.Field[T]{Value: value, Present: true} } 14 | 15 | // Null is a param field helper which explicitly sends null to the API. 16 | func Null[T any]() param.Field[T] { return param.Field[T]{Null: true, Present: true} } 17 | 18 | // Raw is a param field helper for specifying values for fields when the 19 | // type you are looking to send is different from the type that is specified in 20 | // the SDK. For example, if the type of the field is an integer, but you want 21 | // to send a float, you could do that by setting the corresponding field with 22 | // Raw[int](0.5). 23 | func Raw[T any](value any) param.Field[T] { return param.Field[T]{Raw: value, Present: true} } 24 | 25 | // Int is a param field helper which helps specify integers. This is 26 | // particularly helpful when specifying integer constants for fields. 27 | func Int(value int64) param.Field[int64] { return F(value) } 28 | 29 | // String is a param field helper which helps specify strings. 30 | func String(value string) param.Field[string] { return F(value) } 31 | 32 | // Float is a param field helper which helps specify floats. 33 | func Float(value float64) param.Field[float64] { return F(value) } 34 | 35 | // Bool is a param field helper which helps specify bools. 36 | func Bool(value bool) param.Field[bool] { return F(value) } 37 | 38 | // FileParam is a param field helper which helps files with a mime content-type. 39 | func FileParam(reader io.Reader, filename string, contentType string) param.Field[io.Reader] { 40 | return F[io.Reader](&file{reader, filename, contentType}) 41 | } 42 | 43 | type file struct { 44 | io.Reader 45 | name string 46 | contentType string 47 | } 48 | 49 | func (f *file) ContentType() string { return f.contentType } 50 | func (f *file) Filename() string { return f.name } 51 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/terminaldotshop/terminal-sdk-go 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/tidwall/gjson v1.14.4 7 | github.com/tidwall/sjson v1.2.5 8 | ) 9 | 10 | require ( 11 | github.com/tidwall/match v1.1.1 // indirect 12 | github.com/tidwall/pretty v1.2.1 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 2 | github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= 3 | github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 4 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 5 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 6 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 7 | github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= 8 | github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 9 | github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= 10 | github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= 11 | -------------------------------------------------------------------------------- /internal/apierror/apierror.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package apierror 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | "net/http/httputil" 9 | 10 | "github.com/terminaldotshop/terminal-sdk-go/internal/apijson" 11 | ) 12 | 13 | // Error represents an error that originates from the API, i.e. when a request is 14 | // made and the API returns a response with a HTTP status code. Other errors are 15 | // not wrapped by this SDK. 16 | type Error struct { 17 | JSON errorJSON `json:"-"` 18 | StatusCode int 19 | Request *http.Request 20 | Response *http.Response 21 | } 22 | 23 | // errorJSON contains the JSON metadata for the struct [Error] 24 | type errorJSON struct { 25 | raw string 26 | ExtraFields map[string]apijson.Field 27 | } 28 | 29 | func (r *Error) UnmarshalJSON(data []byte) (err error) { 30 | return apijson.UnmarshalRoot(data, r) 31 | } 32 | 33 | func (r errorJSON) RawJSON() string { 34 | return r.raw 35 | } 36 | 37 | func (r *Error) Error() string { 38 | // Attempt to re-populate the response body 39 | return fmt.Sprintf("%s \"%s\": %d %s %s", r.Request.Method, r.Request.URL, r.Response.StatusCode, http.StatusText(r.Response.StatusCode), r.JSON.RawJSON()) 40 | } 41 | 42 | func (r *Error) DumpRequest(body bool) []byte { 43 | if r.Request.GetBody != nil { 44 | r.Request.Body, _ = r.Request.GetBody() 45 | } 46 | out, _ := httputil.DumpRequestOut(r.Request, body) 47 | return out 48 | } 49 | 50 | func (r *Error) DumpResponse(body bool) []byte { 51 | out, _ := httputil.DumpResponse(r.Response, body) 52 | return out 53 | } 54 | -------------------------------------------------------------------------------- /internal/apiform/form.go: -------------------------------------------------------------------------------- 1 | package apiform 2 | 3 | type Marshaler interface { 4 | MarshalMultipart() ([]byte, string, error) 5 | } 6 | -------------------------------------------------------------------------------- /internal/apiform/form_test.go: -------------------------------------------------------------------------------- 1 | package apiform 2 | 3 | import ( 4 | "bytes" 5 | "mime/multipart" 6 | "strings" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func P[T any](v T) *T { return &v } 12 | 13 | type Primitives struct { 14 | A bool `form:"a"` 15 | B int `form:"b"` 16 | C uint `form:"c"` 17 | D float64 `form:"d"` 18 | E float32 `form:"e"` 19 | F []int `form:"f"` 20 | } 21 | 22 | type PrimitivePointers struct { 23 | A *bool `form:"a"` 24 | B *int `form:"b"` 25 | C *uint `form:"c"` 26 | D *float64 `form:"d"` 27 | E *float32 `form:"e"` 28 | F *[]int `form:"f"` 29 | } 30 | 31 | type Slices struct { 32 | Slice []Primitives `form:"slices"` 33 | } 34 | 35 | type DateTime struct { 36 | Date time.Time `form:"date" format:"date"` 37 | DateTime time.Time `form:"date-time" format:"date-time"` 38 | } 39 | 40 | type AdditionalProperties struct { 41 | A bool `form:"a"` 42 | Extras map[string]interface{} `form:"-,extras"` 43 | } 44 | 45 | type TypedAdditionalProperties struct { 46 | A bool `form:"a"` 47 | Extras map[string]int `form:"-,extras"` 48 | } 49 | 50 | type EmbeddedStructs struct { 51 | AdditionalProperties 52 | A *int `form:"number2"` 53 | Extras map[string]interface{} `form:"-,extras"` 54 | } 55 | 56 | type Recursive struct { 57 | Name string `form:"name"` 58 | Child *Recursive `form:"child"` 59 | } 60 | 61 | type UnknownStruct struct { 62 | Unknown interface{} `form:"unknown"` 63 | } 64 | 65 | type UnionStruct struct { 66 | Union Union `form:"union" format:"date"` 67 | } 68 | 69 | type Union interface { 70 | union() 71 | } 72 | 73 | type UnionInteger int64 74 | 75 | func (UnionInteger) union() {} 76 | 77 | type UnionStructA struct { 78 | Type string `form:"type"` 79 | A string `form:"a"` 80 | B string `form:"b"` 81 | } 82 | 83 | func (UnionStructA) union() {} 84 | 85 | type UnionStructB struct { 86 | Type string `form:"type"` 87 | A string `form:"a"` 88 | } 89 | 90 | func (UnionStructB) union() {} 91 | 92 | type UnionTime time.Time 93 | 94 | func (UnionTime) union() {} 95 | 96 | type ReaderStruct struct { 97 | } 98 | 99 | var tests = map[string]struct { 100 | buf string 101 | val interface{} 102 | }{ 103 | "map_string": { 104 | `--xxx 105 | Content-Disposition: form-data; name="foo" 106 | 107 | bar 108 | --xxx-- 109 | `, 110 | map[string]string{"foo": "bar"}, 111 | }, 112 | 113 | "map_interface": { 114 | `--xxx 115 | Content-Disposition: form-data; name="a" 116 | 117 | 1 118 | --xxx 119 | Content-Disposition: form-data; name="b" 120 | 121 | str 122 | --xxx 123 | Content-Disposition: form-data; name="c" 124 | 125 | false 126 | --xxx-- 127 | `, 128 | map[string]interface{}{"a": float64(1), "b": "str", "c": false}, 129 | }, 130 | 131 | "primitive_struct": { 132 | `--xxx 133 | Content-Disposition: form-data; name="a" 134 | 135 | false 136 | --xxx 137 | Content-Disposition: form-data; name="b" 138 | 139 | 237628372683 140 | --xxx 141 | Content-Disposition: form-data; name="c" 142 | 143 | 654 144 | --xxx 145 | Content-Disposition: form-data; name="d" 146 | 147 | 9999.43 148 | --xxx 149 | Content-Disposition: form-data; name="e" 150 | 151 | 43.76 152 | --xxx 153 | Content-Disposition: form-data; name="f.0" 154 | 155 | 1 156 | --xxx 157 | Content-Disposition: form-data; name="f.1" 158 | 159 | 2 160 | --xxx 161 | Content-Disposition: form-data; name="f.2" 162 | 163 | 3 164 | --xxx 165 | Content-Disposition: form-data; name="f.3" 166 | 167 | 4 168 | --xxx-- 169 | `, 170 | Primitives{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}, 171 | }, 172 | 173 | "slices": { 174 | `--xxx 175 | Content-Disposition: form-data; name="slices.0.a" 176 | 177 | false 178 | --xxx 179 | Content-Disposition: form-data; name="slices.0.b" 180 | 181 | 237628372683 182 | --xxx 183 | Content-Disposition: form-data; name="slices.0.c" 184 | 185 | 654 186 | --xxx 187 | Content-Disposition: form-data; name="slices.0.d" 188 | 189 | 9999.43 190 | --xxx 191 | Content-Disposition: form-data; name="slices.0.e" 192 | 193 | 43.76 194 | --xxx 195 | Content-Disposition: form-data; name="slices.0.f.0" 196 | 197 | 1 198 | --xxx 199 | Content-Disposition: form-data; name="slices.0.f.1" 200 | 201 | 2 202 | --xxx 203 | Content-Disposition: form-data; name="slices.0.f.2" 204 | 205 | 3 206 | --xxx 207 | Content-Disposition: form-data; name="slices.0.f.3" 208 | 209 | 4 210 | --xxx-- 211 | `, 212 | Slices{ 213 | Slice: []Primitives{{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}}, 214 | }, 215 | }, 216 | 217 | "primitive_pointer_struct": { 218 | `--xxx 219 | Content-Disposition: form-data; name="a" 220 | 221 | false 222 | --xxx 223 | Content-Disposition: form-data; name="b" 224 | 225 | 237628372683 226 | --xxx 227 | Content-Disposition: form-data; name="c" 228 | 229 | 654 230 | --xxx 231 | Content-Disposition: form-data; name="d" 232 | 233 | 9999.43 234 | --xxx 235 | Content-Disposition: form-data; name="e" 236 | 237 | 43.76 238 | --xxx 239 | Content-Disposition: form-data; name="f.0" 240 | 241 | 1 242 | --xxx 243 | Content-Disposition: form-data; name="f.1" 244 | 245 | 2 246 | --xxx 247 | Content-Disposition: form-data; name="f.2" 248 | 249 | 3 250 | --xxx 251 | Content-Disposition: form-data; name="f.3" 252 | 253 | 4 254 | --xxx 255 | Content-Disposition: form-data; name="f.4" 256 | 257 | 5 258 | --xxx-- 259 | `, 260 | PrimitivePointers{ 261 | A: P(false), 262 | B: P(237628372683), 263 | C: P(uint(654)), 264 | D: P(9999.43), 265 | E: P(float32(43.76)), 266 | F: &[]int{1, 2, 3, 4, 5}, 267 | }, 268 | }, 269 | 270 | "datetime_struct": { 271 | `--xxx 272 | Content-Disposition: form-data; name="date" 273 | 274 | 2006-01-02 275 | --xxx 276 | Content-Disposition: form-data; name="date-time" 277 | 278 | 2006-01-02T15:04:05Z 279 | --xxx-- 280 | `, 281 | DateTime{ 282 | Date: time.Date(2006, time.January, 2, 0, 0, 0, 0, time.UTC), 283 | DateTime: time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC), 284 | }, 285 | }, 286 | 287 | "additional_properties": { 288 | `--xxx 289 | Content-Disposition: form-data; name="a" 290 | 291 | true 292 | --xxx 293 | Content-Disposition: form-data; name="bar" 294 | 295 | value 296 | --xxx 297 | Content-Disposition: form-data; name="foo" 298 | 299 | true 300 | --xxx-- 301 | `, 302 | AdditionalProperties{ 303 | A: true, 304 | Extras: map[string]interface{}{ 305 | "bar": "value", 306 | "foo": true, 307 | }, 308 | }, 309 | }, 310 | 311 | "recursive_struct": { 312 | `--xxx 313 | Content-Disposition: form-data; name="child.name" 314 | 315 | Alex 316 | --xxx 317 | Content-Disposition: form-data; name="name" 318 | 319 | Robert 320 | --xxx-- 321 | `, 322 | Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}}, 323 | }, 324 | 325 | "unknown_struct_number": { 326 | `--xxx 327 | Content-Disposition: form-data; name="unknown" 328 | 329 | 12 330 | --xxx-- 331 | `, 332 | UnknownStruct{ 333 | Unknown: 12., 334 | }, 335 | }, 336 | 337 | "unknown_struct_map": { 338 | `--xxx 339 | Content-Disposition: form-data; name="unknown.foo" 340 | 341 | bar 342 | --xxx-- 343 | `, 344 | UnknownStruct{ 345 | Unknown: map[string]interface{}{ 346 | "foo": "bar", 347 | }, 348 | }, 349 | }, 350 | 351 | "union_integer": { 352 | `--xxx 353 | Content-Disposition: form-data; name="union" 354 | 355 | 12 356 | --xxx-- 357 | `, 358 | UnionStruct{ 359 | Union: UnionInteger(12), 360 | }, 361 | }, 362 | 363 | "union_struct_discriminated_a": { 364 | `--xxx 365 | Content-Disposition: form-data; name="union.a" 366 | 367 | foo 368 | --xxx 369 | Content-Disposition: form-data; name="union.b" 370 | 371 | bar 372 | --xxx 373 | Content-Disposition: form-data; name="union.type" 374 | 375 | typeA 376 | --xxx-- 377 | `, 378 | 379 | UnionStruct{ 380 | Union: UnionStructA{ 381 | Type: "typeA", 382 | A: "foo", 383 | B: "bar", 384 | }, 385 | }, 386 | }, 387 | 388 | "union_struct_discriminated_b": { 389 | `--xxx 390 | Content-Disposition: form-data; name="union.a" 391 | 392 | foo 393 | --xxx 394 | Content-Disposition: form-data; name="union.type" 395 | 396 | typeB 397 | --xxx-- 398 | `, 399 | UnionStruct{ 400 | Union: UnionStructB{ 401 | Type: "typeB", 402 | A: "foo", 403 | }, 404 | }, 405 | }, 406 | 407 | "union_struct_time": { 408 | `--xxx 409 | Content-Disposition: form-data; name="union" 410 | 411 | 2010-05-23 412 | --xxx-- 413 | `, 414 | UnionStruct{ 415 | Union: UnionTime(time.Date(2010, 05, 23, 0, 0, 0, 0, time.UTC)), 416 | }, 417 | }, 418 | } 419 | 420 | func TestEncode(t *testing.T) { 421 | for name, test := range tests { 422 | t.Run(name, func(t *testing.T) { 423 | buf := bytes.NewBuffer(nil) 424 | writer := multipart.NewWriter(buf) 425 | writer.SetBoundary("xxx") 426 | err := Marshal(test.val, writer) 427 | if err != nil { 428 | t.Errorf("serialization of %v failed with error %v", test.val, err) 429 | } 430 | err = writer.Close() 431 | if err != nil { 432 | t.Errorf("serialization of %v failed with error %v", test.val, err) 433 | } 434 | raw := buf.Bytes() 435 | if string(raw) != strings.ReplaceAll(test.buf, "\n", "\r\n") { 436 | t.Errorf("expected %+#v to serialize to '%s' but got '%s'", test.val, test.buf, string(raw)) 437 | } 438 | }) 439 | } 440 | } 441 | -------------------------------------------------------------------------------- /internal/apiform/tag.go: -------------------------------------------------------------------------------- 1 | package apiform 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | ) 7 | 8 | const jsonStructTag = "json" 9 | const formStructTag = "form" 10 | const formatStructTag = "format" 11 | 12 | type parsedStructTag struct { 13 | name string 14 | required bool 15 | extras bool 16 | metadata bool 17 | } 18 | 19 | func parseFormStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) { 20 | raw, ok := field.Tag.Lookup(formStructTag) 21 | if !ok { 22 | raw, ok = field.Tag.Lookup(jsonStructTag) 23 | } 24 | if !ok { 25 | return 26 | } 27 | parts := strings.Split(raw, ",") 28 | if len(parts) == 0 { 29 | return tag, false 30 | } 31 | tag.name = parts[0] 32 | for _, part := range parts[1:] { 33 | switch part { 34 | case "required": 35 | tag.required = true 36 | case "extras": 37 | tag.extras = true 38 | case "metadata": 39 | tag.metadata = true 40 | } 41 | } 42 | return 43 | } 44 | 45 | func parseFormatStructTag(field reflect.StructField) (format string, ok bool) { 46 | format, ok = field.Tag.Lookup(formatStructTag) 47 | return 48 | } 49 | -------------------------------------------------------------------------------- /internal/apijson/encoder.go: -------------------------------------------------------------------------------- 1 | package apijson 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "reflect" 8 | "sort" 9 | "strconv" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | "github.com/tidwall/sjson" 15 | 16 | "github.com/terminaldotshop/terminal-sdk-go/internal/param" 17 | ) 18 | 19 | var encoders sync.Map // map[encoderEntry]encoderFunc 20 | 21 | // If we want to set a literal key value into JSON using sjson, we need to make sure it doesn't have 22 | // special characters that sjson interprets as a path. 23 | var EscapeSJSONKey = strings.NewReplacer("\\", "\\\\", "|", "\\|", "#", "\\#", "@", "\\@", "*", "\\*", ".", "\\.", ":", "\\:", "?", "\\?").Replace 24 | 25 | func Marshal(value interface{}) ([]byte, error) { 26 | e := &encoder{dateFormat: time.RFC3339} 27 | return e.marshal(value) 28 | } 29 | 30 | func MarshalRoot(value interface{}) ([]byte, error) { 31 | e := &encoder{root: true, dateFormat: time.RFC3339} 32 | return e.marshal(value) 33 | } 34 | 35 | type encoder struct { 36 | dateFormat string 37 | root bool 38 | } 39 | 40 | type encoderFunc func(value reflect.Value) ([]byte, error) 41 | 42 | type encoderField struct { 43 | tag parsedStructTag 44 | fn encoderFunc 45 | idx []int 46 | } 47 | 48 | type encoderEntry struct { 49 | reflect.Type 50 | dateFormat string 51 | root bool 52 | } 53 | 54 | func (e *encoder) marshal(value interface{}) ([]byte, error) { 55 | val := reflect.ValueOf(value) 56 | if !val.IsValid() { 57 | return nil, nil 58 | } 59 | typ := val.Type() 60 | enc := e.typeEncoder(typ) 61 | return enc(val) 62 | } 63 | 64 | func (e *encoder) typeEncoder(t reflect.Type) encoderFunc { 65 | entry := encoderEntry{ 66 | Type: t, 67 | dateFormat: e.dateFormat, 68 | root: e.root, 69 | } 70 | 71 | if fi, ok := encoders.Load(entry); ok { 72 | return fi.(encoderFunc) 73 | } 74 | 75 | // To deal with recursive types, populate the map with an 76 | // indirect func before we build it. This type waits on the 77 | // real func (f) to be ready and then calls it. This indirect 78 | // func is only used for recursive types. 79 | var ( 80 | wg sync.WaitGroup 81 | f encoderFunc 82 | ) 83 | wg.Add(1) 84 | fi, loaded := encoders.LoadOrStore(entry, encoderFunc(func(v reflect.Value) ([]byte, error) { 85 | wg.Wait() 86 | return f(v) 87 | })) 88 | if loaded { 89 | return fi.(encoderFunc) 90 | } 91 | 92 | // Compute the real encoder and replace the indirect func with it. 93 | f = e.newTypeEncoder(t) 94 | wg.Done() 95 | encoders.Store(entry, f) 96 | return f 97 | } 98 | 99 | func marshalerEncoder(v reflect.Value) ([]byte, error) { 100 | return v.Interface().(json.Marshaler).MarshalJSON() 101 | } 102 | 103 | func indirectMarshalerEncoder(v reflect.Value) ([]byte, error) { 104 | return v.Addr().Interface().(json.Marshaler).MarshalJSON() 105 | } 106 | 107 | func (e *encoder) newTypeEncoder(t reflect.Type) encoderFunc { 108 | if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { 109 | return e.newTimeTypeEncoder() 110 | } 111 | if !e.root && t.Implements(reflect.TypeOf((*json.Marshaler)(nil)).Elem()) { 112 | return marshalerEncoder 113 | } 114 | if !e.root && reflect.PointerTo(t).Implements(reflect.TypeOf((*json.Marshaler)(nil)).Elem()) { 115 | return indirectMarshalerEncoder 116 | } 117 | e.root = false 118 | switch t.Kind() { 119 | case reflect.Pointer: 120 | inner := t.Elem() 121 | 122 | innerEncoder := e.typeEncoder(inner) 123 | return func(v reflect.Value) ([]byte, error) { 124 | if !v.IsValid() || v.IsNil() { 125 | return nil, nil 126 | } 127 | return innerEncoder(v.Elem()) 128 | } 129 | case reflect.Struct: 130 | return e.newStructTypeEncoder(t) 131 | case reflect.Array: 132 | fallthrough 133 | case reflect.Slice: 134 | return e.newArrayTypeEncoder(t) 135 | case reflect.Map: 136 | return e.newMapEncoder(t) 137 | case reflect.Interface: 138 | return e.newInterfaceEncoder() 139 | default: 140 | return e.newPrimitiveTypeEncoder(t) 141 | } 142 | } 143 | 144 | func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc { 145 | switch t.Kind() { 146 | // Note that we could use `gjson` to encode these types but it would complicate our 147 | // code more and this current code shouldn't cause any issues 148 | case reflect.String: 149 | return func(v reflect.Value) ([]byte, error) { 150 | return json.Marshal(v.Interface()) 151 | } 152 | case reflect.Bool: 153 | return func(v reflect.Value) ([]byte, error) { 154 | if v.Bool() { 155 | return []byte("true"), nil 156 | } 157 | return []byte("false"), nil 158 | } 159 | case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64: 160 | return func(v reflect.Value) ([]byte, error) { 161 | return []byte(strconv.FormatInt(v.Int(), 10)), nil 162 | } 163 | case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: 164 | return func(v reflect.Value) ([]byte, error) { 165 | return []byte(strconv.FormatUint(v.Uint(), 10)), nil 166 | } 167 | case reflect.Float32: 168 | return func(v reflect.Value) ([]byte, error) { 169 | return []byte(strconv.FormatFloat(v.Float(), 'f', -1, 32)), nil 170 | } 171 | case reflect.Float64: 172 | return func(v reflect.Value) ([]byte, error) { 173 | return []byte(strconv.FormatFloat(v.Float(), 'f', -1, 64)), nil 174 | } 175 | default: 176 | return func(v reflect.Value) ([]byte, error) { 177 | return nil, fmt.Errorf("unknown type received at primitive encoder: %s", t.String()) 178 | } 179 | } 180 | } 181 | 182 | func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc { 183 | itemEncoder := e.typeEncoder(t.Elem()) 184 | 185 | return func(value reflect.Value) ([]byte, error) { 186 | json := []byte("[]") 187 | for i := 0; i < value.Len(); i++ { 188 | var value, err = itemEncoder(value.Index(i)) 189 | if err != nil { 190 | return nil, err 191 | } 192 | if value == nil { 193 | // Assume that empty items should be inserted as `null` so that the output array 194 | // will be the same length as the input array 195 | value = []byte("null") 196 | } 197 | 198 | json, err = sjson.SetRawBytes(json, "-1", value) 199 | if err != nil { 200 | return nil, err 201 | } 202 | } 203 | 204 | return json, nil 205 | } 206 | } 207 | 208 | func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { 209 | if t.Implements(reflect.TypeOf((*param.FieldLike)(nil)).Elem()) { 210 | return e.newFieldTypeEncoder(t) 211 | } 212 | 213 | encoderFields := []encoderField{} 214 | extraEncoder := (*encoderField)(nil) 215 | 216 | // This helper allows us to recursively collect field encoders into a flat 217 | // array. The parameter `index` keeps track of the access patterns necessary 218 | // to get to some field. 219 | var collectEncoderFields func(r reflect.Type, index []int) 220 | collectEncoderFields = func(r reflect.Type, index []int) { 221 | for i := 0; i < r.NumField(); i++ { 222 | idx := append(index, i) 223 | field := t.FieldByIndex(idx) 224 | if !field.IsExported() { 225 | continue 226 | } 227 | // If this is an embedded struct, traverse one level deeper to extract 228 | // the field and get their encoders as well. 229 | if field.Anonymous { 230 | collectEncoderFields(field.Type, idx) 231 | continue 232 | } 233 | // If json tag is not present, then we skip, which is intentionally 234 | // different behavior from the stdlib. 235 | ptag, ok := parseJSONStructTag(field) 236 | if !ok { 237 | continue 238 | } 239 | // We only want to support unexported field if they're tagged with 240 | // `extras` because that field shouldn't be part of the public API. We 241 | // also want to only keep the top level extras 242 | if ptag.extras && len(index) == 0 { 243 | extraEncoder = &encoderField{ptag, e.typeEncoder(field.Type.Elem()), idx} 244 | continue 245 | } 246 | if ptag.name == "-" { 247 | continue 248 | } 249 | 250 | dateFormat, ok := parseFormatStructTag(field) 251 | oldFormat := e.dateFormat 252 | if ok { 253 | switch dateFormat { 254 | case "date-time": 255 | e.dateFormat = time.RFC3339 256 | case "date": 257 | e.dateFormat = "2006-01-02" 258 | } 259 | } 260 | encoderFields = append(encoderFields, encoderField{ptag, e.typeEncoder(field.Type), idx}) 261 | e.dateFormat = oldFormat 262 | } 263 | } 264 | collectEncoderFields(t, []int{}) 265 | 266 | // Ensure deterministic output by sorting by lexicographic order 267 | sort.Slice(encoderFields, func(i, j int) bool { 268 | return encoderFields[i].tag.name < encoderFields[j].tag.name 269 | }) 270 | 271 | return func(value reflect.Value) (json []byte, err error) { 272 | json = []byte("{}") 273 | 274 | for _, ef := range encoderFields { 275 | field := value.FieldByIndex(ef.idx) 276 | encoded, err := ef.fn(field) 277 | if err != nil { 278 | return nil, err 279 | } 280 | if encoded == nil { 281 | continue 282 | } 283 | json, err = sjson.SetRawBytes(json, EscapeSJSONKey(ef.tag.name), encoded) 284 | if err != nil { 285 | return nil, err 286 | } 287 | } 288 | 289 | if extraEncoder != nil { 290 | json, err = e.encodeMapEntries(json, value.FieldByIndex(extraEncoder.idx)) 291 | if err != nil { 292 | return nil, err 293 | } 294 | } 295 | return 296 | } 297 | } 298 | 299 | func (e *encoder) newFieldTypeEncoder(t reflect.Type) encoderFunc { 300 | f, _ := t.FieldByName("Value") 301 | enc := e.typeEncoder(f.Type) 302 | 303 | return func(value reflect.Value) (json []byte, err error) { 304 | present := value.FieldByName("Present") 305 | if !present.Bool() { 306 | return nil, nil 307 | } 308 | null := value.FieldByName("Null") 309 | if null.Bool() { 310 | return []byte("null"), nil 311 | } 312 | raw := value.FieldByName("Raw") 313 | if !raw.IsNil() { 314 | return e.typeEncoder(raw.Type())(raw) 315 | } 316 | return enc(value.FieldByName("Value")) 317 | } 318 | } 319 | 320 | func (e *encoder) newTimeTypeEncoder() encoderFunc { 321 | format := e.dateFormat 322 | return func(value reflect.Value) (json []byte, err error) { 323 | return []byte(`"` + value.Convert(reflect.TypeOf(time.Time{})).Interface().(time.Time).Format(format) + `"`), nil 324 | } 325 | } 326 | 327 | func (e encoder) newInterfaceEncoder() encoderFunc { 328 | return func(value reflect.Value) ([]byte, error) { 329 | value = value.Elem() 330 | if !value.IsValid() { 331 | return nil, nil 332 | } 333 | return e.typeEncoder(value.Type())(value) 334 | } 335 | } 336 | 337 | // Given a []byte of json (may either be an empty object or an object that already contains entries) 338 | // encode all of the entries in the map to the json byte array. 339 | func (e *encoder) encodeMapEntries(json []byte, v reflect.Value) ([]byte, error) { 340 | type mapPair struct { 341 | key []byte 342 | value reflect.Value 343 | } 344 | 345 | pairs := []mapPair{} 346 | keyEncoder := e.typeEncoder(v.Type().Key()) 347 | 348 | iter := v.MapRange() 349 | for iter.Next() { 350 | var encodedKeyString string 351 | if iter.Key().Type().Kind() == reflect.String { 352 | encodedKeyString = iter.Key().String() 353 | } else { 354 | var err error 355 | encodedKeyBytes, err := keyEncoder(iter.Key()) 356 | if err != nil { 357 | return nil, err 358 | } 359 | encodedKeyString = string(encodedKeyBytes) 360 | } 361 | encodedKey := []byte(encodedKeyString) 362 | pairs = append(pairs, mapPair{key: encodedKey, value: iter.Value()}) 363 | } 364 | 365 | // Ensure deterministic output 366 | sort.Slice(pairs, func(i, j int) bool { 367 | return bytes.Compare(pairs[i].key, pairs[j].key) < 0 368 | }) 369 | 370 | elementEncoder := e.typeEncoder(v.Type().Elem()) 371 | for _, p := range pairs { 372 | encodedValue, err := elementEncoder(p.value) 373 | if err != nil { 374 | return nil, err 375 | } 376 | if len(encodedValue) == 0 { 377 | continue 378 | } 379 | json, err = sjson.SetRawBytes(json, EscapeSJSONKey(string(p.key)), encodedValue) 380 | if err != nil { 381 | return nil, err 382 | } 383 | } 384 | 385 | return json, nil 386 | } 387 | 388 | func (e *encoder) newMapEncoder(t reflect.Type) encoderFunc { 389 | return func(value reflect.Value) ([]byte, error) { 390 | json := []byte("{}") 391 | var err error 392 | json, err = e.encodeMapEntries(json, value) 393 | if err != nil { 394 | return nil, err 395 | } 396 | return json, nil 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /internal/apijson/field.go: -------------------------------------------------------------------------------- 1 | package apijson 2 | 3 | import "reflect" 4 | 5 | type status uint8 6 | 7 | const ( 8 | missing status = iota 9 | null 10 | invalid 11 | valid 12 | ) 13 | 14 | type Field struct { 15 | raw string 16 | status status 17 | } 18 | 19 | // Returns true if the field is explicitly `null` _or_ if it is not present at all (ie, missing). 20 | // To check if the field's key is present in the JSON with an explicit null value, 21 | // you must check `f.IsNull() && !f.IsMissing()`. 22 | func (j Field) IsNull() bool { return j.status <= null } 23 | func (j Field) IsMissing() bool { return j.status == missing } 24 | func (j Field) IsInvalid() bool { return j.status == invalid } 25 | func (j Field) Raw() string { return j.raw } 26 | 27 | func getSubField(root reflect.Value, index []int, name string) reflect.Value { 28 | strct := root.FieldByIndex(index[:len(index)-1]) 29 | if !strct.IsValid() { 30 | panic("couldn't find encapsulating struct for field " + name) 31 | } 32 | meta := strct.FieldByName("JSON") 33 | if !meta.IsValid() { 34 | return reflect.Value{} 35 | } 36 | field := meta.FieldByName(name) 37 | if !field.IsValid() { 38 | return reflect.Value{} 39 | } 40 | return field 41 | } 42 | -------------------------------------------------------------------------------- /internal/apijson/field_test.go: -------------------------------------------------------------------------------- 1 | package apijson 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/terminaldotshop/terminal-sdk-go/internal/param" 8 | ) 9 | 10 | type Struct struct { 11 | A string `json:"a"` 12 | B int64 `json:"b"` 13 | } 14 | 15 | type FieldStruct struct { 16 | A param.Field[string] `json:"a"` 17 | B param.Field[int64] `json:"b"` 18 | C param.Field[Struct] `json:"c"` 19 | D param.Field[time.Time] `json:"d" format:"date"` 20 | E param.Field[time.Time] `json:"e" format:"date-time"` 21 | F param.Field[int64] `json:"f"` 22 | } 23 | 24 | func TestFieldMarshal(t *testing.T) { 25 | tests := map[string]struct { 26 | value interface{} 27 | expected string 28 | }{ 29 | "null_string": {param.Field[string]{Present: true, Null: true}, "null"}, 30 | "null_int": {param.Field[int]{Present: true, Null: true}, "null"}, 31 | "null_int64": {param.Field[int64]{Present: true, Null: true}, "null"}, 32 | "null_struct": {param.Field[Struct]{Present: true, Null: true}, "null"}, 33 | 34 | "string": {param.Field[string]{Present: true, Value: "string"}, `"string"`}, 35 | "int": {param.Field[int]{Present: true, Value: 123}, "123"}, 36 | "int64": {param.Field[int64]{Present: true, Value: int64(123456789123456789)}, "123456789123456789"}, 37 | "struct": {param.Field[Struct]{Present: true, Value: Struct{A: "yo", B: 123}}, `{"a":"yo","b":123}`}, 38 | 39 | "string_raw": {param.Field[int]{Present: true, Raw: "string"}, `"string"`}, 40 | "int_raw": {param.Field[int]{Present: true, Raw: 123}, "123"}, 41 | "int64_raw": {param.Field[int]{Present: true, Raw: int64(123456789123456789)}, "123456789123456789"}, 42 | "struct_raw": {param.Field[int]{Present: true, Raw: Struct{A: "yo", B: 123}}, `{"a":"yo","b":123}`}, 43 | 44 | "param_struct": { 45 | FieldStruct{ 46 | A: param.Field[string]{Present: true, Value: "hello"}, 47 | B: param.Field[int64]{Present: true, Value: int64(12)}, 48 | D: param.Field[time.Time]{Present: true, Value: time.Date(2023, time.March, 18, 14, 47, 38, 0, time.UTC)}, 49 | E: param.Field[time.Time]{Present: true, Value: time.Date(2023, time.March, 18, 14, 47, 38, 0, time.UTC)}, 50 | }, 51 | `{"a":"hello","b":12,"d":"2023-03-18","e":"2023-03-18T14:47:38Z"}`, 52 | }, 53 | } 54 | 55 | for name, test := range tests { 56 | t.Run(name, func(t *testing.T) { 57 | b, err := Marshal(test.value) 58 | if err != nil { 59 | t.Fatalf("didn't expect error %v", err) 60 | } 61 | if string(b) != test.expected { 62 | t.Fatalf("expected %s, received %s", test.expected, string(b)) 63 | } 64 | }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /internal/apijson/port.go: -------------------------------------------------------------------------------- 1 | package apijson 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | // Port copies over values from one struct to another struct. 9 | func Port(from any, to any) error { 10 | toVal := reflect.ValueOf(to) 11 | fromVal := reflect.ValueOf(from) 12 | 13 | if toVal.Kind() != reflect.Ptr || toVal.IsNil() { 14 | return fmt.Errorf("destination must be a non-nil pointer") 15 | } 16 | 17 | for toVal.Kind() == reflect.Ptr { 18 | toVal = toVal.Elem() 19 | } 20 | toType := toVal.Type() 21 | 22 | for fromVal.Kind() == reflect.Ptr { 23 | fromVal = fromVal.Elem() 24 | } 25 | fromType := fromVal.Type() 26 | 27 | if toType.Kind() != reflect.Struct { 28 | return fmt.Errorf("destination must be a non-nil pointer to a struct (%v %v)", toType, toType.Kind()) 29 | } 30 | 31 | values := map[string]reflect.Value{} 32 | fields := map[string]reflect.Value{} 33 | 34 | fromJSON := fromVal.FieldByName("JSON") 35 | toJSON := toVal.FieldByName("JSON") 36 | 37 | // Iterate through the fields of v and load all the "normal" fields in the struct to the map of 38 | // string to reflect.Value, as well as their raw .JSON.Foo counterpart indicated by j. 39 | var getFields func(t reflect.Type, v reflect.Value) 40 | getFields = func(t reflect.Type, v reflect.Value) { 41 | j := v.FieldByName("JSON") 42 | 43 | // Recurse into anonymous fields first, since the fields on the object should win over the fields in the 44 | // embedded object. 45 | for i := 0; i < t.NumField(); i++ { 46 | field := t.Field(i) 47 | if field.Anonymous { 48 | getFields(field.Type, v.Field(i)) 49 | continue 50 | } 51 | } 52 | 53 | for i := 0; i < t.NumField(); i++ { 54 | field := t.Field(i) 55 | ptag, ok := parseJSONStructTag(field) 56 | if !ok || ptag.name == "-" { 57 | continue 58 | } 59 | values[ptag.name] = v.Field(i) 60 | if j.IsValid() { 61 | fields[ptag.name] = j.FieldByName(field.Name) 62 | } 63 | } 64 | } 65 | getFields(fromType, fromVal) 66 | 67 | // Use the values from the previous step to populate the 'to' struct. 68 | for i := 0; i < toType.NumField(); i++ { 69 | field := toType.Field(i) 70 | ptag, ok := parseJSONStructTag(field) 71 | if !ok { 72 | continue 73 | } 74 | if ptag.name == "-" { 75 | continue 76 | } 77 | if value, ok := values[ptag.name]; ok { 78 | delete(values, ptag.name) 79 | if field.Type.Kind() == reflect.Interface { 80 | toVal.Field(i).Set(value) 81 | } else { 82 | switch value.Kind() { 83 | case reflect.String: 84 | toVal.Field(i).SetString(value.String()) 85 | case reflect.Bool: 86 | toVal.Field(i).SetBool(value.Bool()) 87 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 88 | toVal.Field(i).SetInt(value.Int()) 89 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 90 | toVal.Field(i).SetUint(value.Uint()) 91 | case reflect.Float32, reflect.Float64: 92 | toVal.Field(i).SetFloat(value.Float()) 93 | default: 94 | toVal.Field(i).Set(value) 95 | } 96 | } 97 | } 98 | 99 | if fromJSONField, ok := fields[ptag.name]; ok { 100 | if toJSONField := toJSON.FieldByName(field.Name); toJSONField.IsValid() { 101 | toJSONField.Set(fromJSONField) 102 | } 103 | } 104 | } 105 | 106 | // Finally, copy over the .JSON.raw and .JSON.ExtraFields 107 | if toJSON.IsValid() { 108 | if raw := toJSON.FieldByName("raw"); raw.IsValid() { 109 | setUnexportedField(raw, fromJSON.Interface().(interface{ RawJSON() string }).RawJSON()) 110 | } 111 | 112 | if toExtraFields := toJSON.FieldByName("ExtraFields"); toExtraFields.IsValid() { 113 | if fromExtraFields := fromJSON.FieldByName("ExtraFields"); fromExtraFields.IsValid() { 114 | setUnexportedField(toExtraFields, fromExtraFields.Interface()) 115 | } 116 | } 117 | } 118 | 119 | return nil 120 | } 121 | -------------------------------------------------------------------------------- /internal/apijson/port_test.go: -------------------------------------------------------------------------------- 1 | package apijson 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | type Metadata struct { 9 | CreatedAt string `json:"created_at"` 10 | } 11 | 12 | // Card is the "combined" type of CardVisa and CardMastercard 13 | type Card struct { 14 | Processor CardProcessor `json:"processor"` 15 | Data any `json:"data"` 16 | IsFoo bool `json:"is_foo"` 17 | IsBar bool `json:"is_bar"` 18 | Metadata Metadata `json:"metadata"` 19 | Value interface{} `json:"value"` 20 | 21 | JSON cardJSON 22 | } 23 | 24 | type cardJSON struct { 25 | Processor Field 26 | Data Field 27 | IsFoo Field 28 | IsBar Field 29 | Metadata Field 30 | Value Field 31 | ExtraFields map[string]Field 32 | raw string 33 | } 34 | 35 | func (r cardJSON) RawJSON() string { return r.raw } 36 | 37 | type CardProcessor string 38 | 39 | // CardVisa 40 | type CardVisa struct { 41 | Processor CardVisaProcessor `json:"processor"` 42 | Data CardVisaData `json:"data"` 43 | IsFoo bool `json:"is_foo"` 44 | Metadata Metadata `json:"metadata"` 45 | Value string `json:"value"` 46 | 47 | JSON cardVisaJSON 48 | } 49 | 50 | type cardVisaJSON struct { 51 | Processor Field 52 | Data Field 53 | IsFoo Field 54 | Metadata Field 55 | Value Field 56 | ExtraFields map[string]Field 57 | raw string 58 | } 59 | 60 | func (r cardVisaJSON) RawJSON() string { return r.raw } 61 | 62 | type CardVisaProcessor string 63 | 64 | type CardVisaData struct { 65 | Foo string `json:"foo"` 66 | } 67 | 68 | // CardMastercard 69 | type CardMastercard struct { 70 | Processor CardMastercardProcessor `json:"processor"` 71 | Data CardMastercardData `json:"data"` 72 | IsBar bool `json:"is_bar"` 73 | Metadata Metadata `json:"metadata"` 74 | Value bool `json:"value"` 75 | 76 | JSON cardMastercardJSON 77 | } 78 | 79 | type cardMastercardJSON struct { 80 | Processor Field 81 | Data Field 82 | IsBar Field 83 | Metadata Field 84 | Value Field 85 | ExtraFields map[string]Field 86 | raw string 87 | } 88 | 89 | func (r cardMastercardJSON) RawJSON() string { return r.raw } 90 | 91 | type CardMastercardProcessor string 92 | 93 | type CardMastercardData struct { 94 | Bar int64 `json:"bar"` 95 | } 96 | 97 | type CommonFields struct { 98 | Metadata Metadata `json:"metadata"` 99 | Value string `json:"value"` 100 | 101 | JSON commonFieldsJSON 102 | } 103 | 104 | type commonFieldsJSON struct { 105 | Metadata Field 106 | Value Field 107 | ExtraFields map[string]Field 108 | raw string 109 | } 110 | 111 | type CardEmbedded struct { 112 | CommonFields 113 | Processor CardVisaProcessor `json:"processor"` 114 | Data CardVisaData `json:"data"` 115 | IsFoo bool `json:"is_foo"` 116 | 117 | JSON cardEmbeddedJSON 118 | } 119 | 120 | type cardEmbeddedJSON struct { 121 | Processor Field 122 | Data Field 123 | IsFoo Field 124 | ExtraFields map[string]Field 125 | raw string 126 | } 127 | 128 | func (r cardEmbeddedJSON) RawJSON() string { return r.raw } 129 | 130 | var portTests = map[string]struct { 131 | from any 132 | to any 133 | }{ 134 | "visa to card": { 135 | CardVisa{ 136 | Processor: "visa", 137 | IsFoo: true, 138 | Data: CardVisaData{ 139 | Foo: "foo", 140 | }, 141 | Metadata: Metadata{ 142 | CreatedAt: "Mar 29 2024", 143 | }, 144 | Value: "value", 145 | JSON: cardVisaJSON{ 146 | raw: `{"processor":"visa","is_foo":true,"data":{"foo":"foo"}}`, 147 | Processor: Field{raw: `"visa"`, status: valid}, 148 | IsFoo: Field{raw: `true`, status: valid}, 149 | Data: Field{raw: `{"foo":"foo"}`, status: valid}, 150 | Value: Field{raw: `"value"`, status: valid}, 151 | ExtraFields: map[string]Field{"extra": {raw: `"yo"`, status: valid}}, 152 | }, 153 | }, 154 | Card{ 155 | Processor: "visa", 156 | IsFoo: true, 157 | IsBar: false, 158 | Data: CardVisaData{ 159 | Foo: "foo", 160 | }, 161 | Metadata: Metadata{ 162 | CreatedAt: "Mar 29 2024", 163 | }, 164 | Value: "value", 165 | JSON: cardJSON{ 166 | raw: `{"processor":"visa","is_foo":true,"data":{"foo":"foo"}}`, 167 | Processor: Field{raw: `"visa"`, status: valid}, 168 | IsFoo: Field{raw: `true`, status: valid}, 169 | Data: Field{raw: `{"foo":"foo"}`, status: valid}, 170 | Value: Field{raw: `"value"`, status: valid}, 171 | ExtraFields: map[string]Field{"extra": {raw: `"yo"`, status: valid}}, 172 | }, 173 | }, 174 | }, 175 | "mastercard to card": { 176 | CardMastercard{ 177 | Processor: "mastercard", 178 | IsBar: true, 179 | Data: CardMastercardData{ 180 | Bar: 13, 181 | }, 182 | Value: false, 183 | }, 184 | Card{ 185 | Processor: "mastercard", 186 | IsFoo: false, 187 | IsBar: true, 188 | Data: CardMastercardData{ 189 | Bar: 13, 190 | }, 191 | Value: false, 192 | }, 193 | }, 194 | "embedded to card": { 195 | CardEmbedded{ 196 | CommonFields: CommonFields{ 197 | Metadata: Metadata{ 198 | CreatedAt: "Mar 29 2024", 199 | }, 200 | Value: "embedded_value", 201 | JSON: commonFieldsJSON{ 202 | Metadata: Field{raw: `{"created_at":"Mar 29 2024"}`, status: valid}, 203 | Value: Field{raw: `"embedded_value"`, status: valid}, 204 | raw: `should not matter`, 205 | }, 206 | }, 207 | Processor: "visa", 208 | IsFoo: true, 209 | Data: CardVisaData{ 210 | Foo: "embedded_foo", 211 | }, 212 | JSON: cardEmbeddedJSON{ 213 | raw: `{"processor":"visa","is_foo":true,"data":{"foo":"embedded_foo"},"metadata":{"created_at":"Mar 29 2024"},"value":"embedded_value"}`, 214 | Processor: Field{raw: `"visa"`, status: valid}, 215 | IsFoo: Field{raw: `true`, status: valid}, 216 | Data: Field{raw: `{"foo":"embedded_foo"}`, status: valid}, 217 | }, 218 | }, 219 | Card{ 220 | Processor: "visa", 221 | IsFoo: true, 222 | IsBar: false, 223 | Data: CardVisaData{ 224 | Foo: "embedded_foo", 225 | }, 226 | Metadata: Metadata{ 227 | CreatedAt: "Mar 29 2024", 228 | }, 229 | Value: "embedded_value", 230 | JSON: cardJSON{ 231 | raw: `{"processor":"visa","is_foo":true,"data":{"foo":"embedded_foo"},"metadata":{"created_at":"Mar 29 2024"},"value":"embedded_value"}`, 232 | Processor: Field{raw: `"visa"`, status: 0x3}, 233 | IsFoo: Field{raw: "true", status: 0x3}, 234 | Data: Field{raw: `{"foo":"embedded_foo"}`, status: 0x3}, 235 | Metadata: Field{raw: `{"created_at":"Mar 29 2024"}`, status: 0x3}, 236 | Value: Field{raw: `"embedded_value"`, status: 0x3}, 237 | }, 238 | }, 239 | }, 240 | } 241 | 242 | func TestPort(t *testing.T) { 243 | for name, test := range portTests { 244 | t.Run(name, func(t *testing.T) { 245 | toVal := reflect.New(reflect.TypeOf(test.to)) 246 | 247 | err := Port(test.from, toVal.Interface()) 248 | if err != nil { 249 | t.Fatalf("port of %v failed with error %v", test.from, err) 250 | } 251 | 252 | if !reflect.DeepEqual(toVal.Elem().Interface(), test.to) { 253 | t.Fatalf("expected:\n%+#v\n\nto port to:\n%+#v\n\nbut got:\n%+#v", test.from, test.to, toVal.Elem().Interface()) 254 | } 255 | }) 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /internal/apijson/registry.go: -------------------------------------------------------------------------------- 1 | package apijson 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/tidwall/gjson" 7 | ) 8 | 9 | type UnionVariant struct { 10 | TypeFilter gjson.Type 11 | DiscriminatorValue interface{} 12 | Type reflect.Type 13 | } 14 | 15 | var unionRegistry = map[reflect.Type]unionEntry{} 16 | var unionVariants = map[reflect.Type]interface{}{} 17 | 18 | type unionEntry struct { 19 | discriminatorKey string 20 | variants []UnionVariant 21 | } 22 | 23 | func RegisterUnion(typ reflect.Type, discriminator string, variants ...UnionVariant) { 24 | unionRegistry[typ] = unionEntry{ 25 | discriminatorKey: discriminator, 26 | variants: variants, 27 | } 28 | for _, variant := range variants { 29 | unionVariants[variant.Type] = typ 30 | } 31 | } 32 | 33 | // Useful to wrap a union type to force it to use [apijson.UnmarshalJSON] since you cannot define an 34 | // UnmarshalJSON function on the interface itself. 35 | type UnionUnmarshaler[T any] struct { 36 | Value T 37 | } 38 | 39 | func (c *UnionUnmarshaler[T]) UnmarshalJSON(buf []byte) error { 40 | return UnmarshalRoot(buf, &c.Value) 41 | } 42 | -------------------------------------------------------------------------------- /internal/apijson/tag.go: -------------------------------------------------------------------------------- 1 | package apijson 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | ) 7 | 8 | const jsonStructTag = "json" 9 | const formatStructTag = "format" 10 | 11 | type parsedStructTag struct { 12 | name string 13 | required bool 14 | extras bool 15 | metadata bool 16 | inline bool 17 | } 18 | 19 | func parseJSONStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) { 20 | raw, ok := field.Tag.Lookup(jsonStructTag) 21 | if !ok { 22 | return 23 | } 24 | parts := strings.Split(raw, ",") 25 | if len(parts) == 0 { 26 | return tag, false 27 | } 28 | tag.name = parts[0] 29 | for _, part := range parts[1:] { 30 | switch part { 31 | case "required": 32 | tag.required = true 33 | case "extras": 34 | tag.extras = true 35 | case "metadata": 36 | tag.metadata = true 37 | case "inline": 38 | tag.inline = true 39 | } 40 | } 41 | return 42 | } 43 | 44 | func parseFormatStructTag(field reflect.StructField) (format string, ok bool) { 45 | format, ok = field.Tag.Lookup(formatStructTag) 46 | return 47 | } 48 | -------------------------------------------------------------------------------- /internal/apiquery/encoder.go: -------------------------------------------------------------------------------- 1 | package apiquery 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "github.com/terminaldotshop/terminal-sdk-go/internal/param" 13 | ) 14 | 15 | var encoders sync.Map // map[reflect.Type]encoderFunc 16 | 17 | type encoder struct { 18 | dateFormat string 19 | root bool 20 | settings QuerySettings 21 | } 22 | 23 | type encoderFunc func(key string, value reflect.Value) []Pair 24 | 25 | type encoderField struct { 26 | tag parsedStructTag 27 | fn encoderFunc 28 | idx []int 29 | } 30 | 31 | type encoderEntry struct { 32 | reflect.Type 33 | dateFormat string 34 | root bool 35 | settings QuerySettings 36 | } 37 | 38 | type Pair struct { 39 | key string 40 | value string 41 | } 42 | 43 | func (e *encoder) typeEncoder(t reflect.Type) encoderFunc { 44 | entry := encoderEntry{ 45 | Type: t, 46 | dateFormat: e.dateFormat, 47 | root: e.root, 48 | settings: e.settings, 49 | } 50 | 51 | if fi, ok := encoders.Load(entry); ok { 52 | return fi.(encoderFunc) 53 | } 54 | 55 | // To deal with recursive types, populate the map with an 56 | // indirect func before we build it. This type waits on the 57 | // real func (f) to be ready and then calls it. This indirect 58 | // func is only used for recursive types. 59 | var ( 60 | wg sync.WaitGroup 61 | f encoderFunc 62 | ) 63 | wg.Add(1) 64 | fi, loaded := encoders.LoadOrStore(entry, encoderFunc(func(key string, v reflect.Value) []Pair { 65 | wg.Wait() 66 | return f(key, v) 67 | })) 68 | if loaded { 69 | return fi.(encoderFunc) 70 | } 71 | 72 | // Compute the real encoder and replace the indirect func with it. 73 | f = e.newTypeEncoder(t) 74 | wg.Done() 75 | encoders.Store(entry, f) 76 | return f 77 | } 78 | 79 | func marshalerEncoder(key string, value reflect.Value) []Pair { 80 | s, _ := value.Interface().(json.Marshaler).MarshalJSON() 81 | return []Pair{{key, string(s)}} 82 | } 83 | 84 | func (e *encoder) newTypeEncoder(t reflect.Type) encoderFunc { 85 | if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { 86 | return e.newTimeTypeEncoder(t) 87 | } 88 | if !e.root && t.Implements(reflect.TypeOf((*json.Marshaler)(nil)).Elem()) { 89 | return marshalerEncoder 90 | } 91 | e.root = false 92 | switch t.Kind() { 93 | case reflect.Pointer: 94 | encoder := e.typeEncoder(t.Elem()) 95 | return func(key string, value reflect.Value) (pairs []Pair) { 96 | if !value.IsValid() || value.IsNil() { 97 | return 98 | } 99 | pairs = encoder(key, value.Elem()) 100 | return 101 | } 102 | case reflect.Struct: 103 | return e.newStructTypeEncoder(t) 104 | case reflect.Array: 105 | fallthrough 106 | case reflect.Slice: 107 | return e.newArrayTypeEncoder(t) 108 | case reflect.Map: 109 | return e.newMapEncoder(t) 110 | case reflect.Interface: 111 | return e.newInterfaceEncoder() 112 | default: 113 | return e.newPrimitiveTypeEncoder(t) 114 | } 115 | } 116 | 117 | func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { 118 | if t.Implements(reflect.TypeOf((*param.FieldLike)(nil)).Elem()) { 119 | return e.newFieldTypeEncoder(t) 120 | } 121 | 122 | encoderFields := []encoderField{} 123 | 124 | // This helper allows us to recursively collect field encoders into a flat 125 | // array. The parameter `index` keeps track of the access patterns necessary 126 | // to get to some field. 127 | var collectEncoderFields func(r reflect.Type, index []int) 128 | collectEncoderFields = func(r reflect.Type, index []int) { 129 | for i := 0; i < r.NumField(); i++ { 130 | idx := append(index, i) 131 | field := t.FieldByIndex(idx) 132 | if !field.IsExported() { 133 | continue 134 | } 135 | // If this is an embedded struct, traverse one level deeper to extract 136 | // the field and get their encoders as well. 137 | if field.Anonymous { 138 | collectEncoderFields(field.Type, idx) 139 | continue 140 | } 141 | // If query tag is not present, then we skip, which is intentionally 142 | // different behavior from the stdlib. 143 | ptag, ok := parseQueryStructTag(field) 144 | if !ok { 145 | continue 146 | } 147 | 148 | if ptag.name == "-" && !ptag.inline { 149 | continue 150 | } 151 | 152 | dateFormat, ok := parseFormatStructTag(field) 153 | oldFormat := e.dateFormat 154 | if ok { 155 | switch dateFormat { 156 | case "date-time": 157 | e.dateFormat = time.RFC3339 158 | case "date": 159 | e.dateFormat = "2006-01-02" 160 | } 161 | } 162 | encoderFields = append(encoderFields, encoderField{ptag, e.typeEncoder(field.Type), idx}) 163 | e.dateFormat = oldFormat 164 | } 165 | } 166 | collectEncoderFields(t, []int{}) 167 | 168 | return func(key string, value reflect.Value) (pairs []Pair) { 169 | for _, ef := range encoderFields { 170 | var subkey string = e.renderKeyPath(key, ef.tag.name) 171 | if ef.tag.inline { 172 | subkey = key 173 | } 174 | 175 | field := value.FieldByIndex(ef.idx) 176 | pairs = append(pairs, ef.fn(subkey, field)...) 177 | } 178 | return 179 | } 180 | } 181 | 182 | func (e *encoder) newMapEncoder(t reflect.Type) encoderFunc { 183 | keyEncoder := e.typeEncoder(t.Key()) 184 | elementEncoder := e.typeEncoder(t.Elem()) 185 | return func(key string, value reflect.Value) (pairs []Pair) { 186 | iter := value.MapRange() 187 | for iter.Next() { 188 | encodedKey := keyEncoder("", iter.Key()) 189 | if len(encodedKey) != 1 { 190 | panic("Unexpected number of parts for encoded map key. Are you using a non-primitive for this map?") 191 | } 192 | subkey := encodedKey[0].value 193 | keyPath := e.renderKeyPath(key, subkey) 194 | pairs = append(pairs, elementEncoder(keyPath, iter.Value())...) 195 | } 196 | return 197 | } 198 | } 199 | 200 | func (e *encoder) renderKeyPath(key string, subkey string) string { 201 | if len(key) == 0 { 202 | return subkey 203 | } 204 | if e.settings.NestedFormat == NestedQueryFormatDots { 205 | return fmt.Sprintf("%s.%s", key, subkey) 206 | } 207 | return fmt.Sprintf("%s[%s]", key, subkey) 208 | } 209 | 210 | func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc { 211 | switch e.settings.ArrayFormat { 212 | case ArrayQueryFormatComma: 213 | innerEncoder := e.typeEncoder(t.Elem()) 214 | return func(key string, v reflect.Value) []Pair { 215 | elements := []string{} 216 | for i := 0; i < v.Len(); i++ { 217 | for _, pair := range innerEncoder("", v.Index(i)) { 218 | elements = append(elements, pair.value) 219 | } 220 | } 221 | if len(elements) == 0 { 222 | return []Pair{} 223 | } 224 | return []Pair{{key, strings.Join(elements, ",")}} 225 | } 226 | case ArrayQueryFormatRepeat: 227 | innerEncoder := e.typeEncoder(t.Elem()) 228 | return func(key string, value reflect.Value) (pairs []Pair) { 229 | for i := 0; i < value.Len(); i++ { 230 | pairs = append(pairs, innerEncoder(key, value.Index(i))...) 231 | } 232 | return pairs 233 | } 234 | case ArrayQueryFormatIndices: 235 | panic("The array indices format is not supported yet") 236 | case ArrayQueryFormatBrackets: 237 | innerEncoder := e.typeEncoder(t.Elem()) 238 | return func(key string, value reflect.Value) []Pair { 239 | pairs := []Pair{} 240 | for i := 0; i < value.Len(); i++ { 241 | pairs = append(pairs, innerEncoder(key+"[]", value.Index(i))...) 242 | } 243 | return pairs 244 | } 245 | default: 246 | panic(fmt.Sprintf("Unknown ArrayFormat value: %d", e.settings.ArrayFormat)) 247 | } 248 | } 249 | 250 | func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc { 251 | switch t.Kind() { 252 | case reflect.Pointer: 253 | inner := t.Elem() 254 | 255 | innerEncoder := e.newPrimitiveTypeEncoder(inner) 256 | return func(key string, v reflect.Value) []Pair { 257 | if !v.IsValid() || v.IsNil() { 258 | return nil 259 | } 260 | return innerEncoder(key, v.Elem()) 261 | } 262 | case reflect.String: 263 | return func(key string, v reflect.Value) []Pair { 264 | return []Pair{{key, v.String()}} 265 | } 266 | case reflect.Bool: 267 | return func(key string, v reflect.Value) []Pair { 268 | if v.Bool() { 269 | return []Pair{{key, "true"}} 270 | } 271 | return []Pair{{key, "false"}} 272 | } 273 | case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64: 274 | return func(key string, v reflect.Value) []Pair { 275 | return []Pair{{key, strconv.FormatInt(v.Int(), 10)}} 276 | } 277 | case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: 278 | return func(key string, v reflect.Value) []Pair { 279 | return []Pair{{key, strconv.FormatUint(v.Uint(), 10)}} 280 | } 281 | case reflect.Float32, reflect.Float64: 282 | return func(key string, v reflect.Value) []Pair { 283 | return []Pair{{key, strconv.FormatFloat(v.Float(), 'f', -1, 64)}} 284 | } 285 | case reflect.Complex64, reflect.Complex128: 286 | bitSize := 64 287 | if t.Kind() == reflect.Complex128 { 288 | bitSize = 128 289 | } 290 | return func(key string, v reflect.Value) []Pair { 291 | return []Pair{{key, strconv.FormatComplex(v.Complex(), 'f', -1, bitSize)}} 292 | } 293 | default: 294 | return func(key string, v reflect.Value) []Pair { 295 | return nil 296 | } 297 | } 298 | } 299 | 300 | func (e *encoder) newFieldTypeEncoder(t reflect.Type) encoderFunc { 301 | f, _ := t.FieldByName("Value") 302 | enc := e.typeEncoder(f.Type) 303 | 304 | return func(key string, value reflect.Value) []Pair { 305 | present := value.FieldByName("Present") 306 | if !present.Bool() { 307 | return nil 308 | } 309 | null := value.FieldByName("Null") 310 | if null.Bool() { 311 | // TODO: Error? 312 | return nil 313 | } 314 | raw := value.FieldByName("Raw") 315 | if !raw.IsNil() { 316 | return e.typeEncoder(raw.Type())(key, raw) 317 | } 318 | return enc(key, value.FieldByName("Value")) 319 | } 320 | } 321 | 322 | func (e *encoder) newTimeTypeEncoder(t reflect.Type) encoderFunc { 323 | format := e.dateFormat 324 | return func(key string, value reflect.Value) []Pair { 325 | return []Pair{{ 326 | key, 327 | value.Convert(reflect.TypeOf(time.Time{})).Interface().(time.Time).Format(format), 328 | }} 329 | } 330 | } 331 | 332 | func (e encoder) newInterfaceEncoder() encoderFunc { 333 | return func(key string, value reflect.Value) []Pair { 334 | value = value.Elem() 335 | if !value.IsValid() { 336 | return nil 337 | } 338 | return e.typeEncoder(value.Type())(key, value) 339 | } 340 | 341 | } 342 | -------------------------------------------------------------------------------- /internal/apiquery/query.go: -------------------------------------------------------------------------------- 1 | package apiquery 2 | 3 | import ( 4 | "net/url" 5 | "reflect" 6 | "time" 7 | ) 8 | 9 | func MarshalWithSettings(value interface{}, settings QuerySettings) url.Values { 10 | e := encoder{time.RFC3339, true, settings} 11 | kv := url.Values{} 12 | val := reflect.ValueOf(value) 13 | if !val.IsValid() { 14 | return nil 15 | } 16 | typ := val.Type() 17 | for _, pair := range e.typeEncoder(typ)("", val) { 18 | kv.Add(pair.key, pair.value) 19 | } 20 | return kv 21 | } 22 | 23 | func Marshal(value interface{}) url.Values { 24 | return MarshalWithSettings(value, QuerySettings{}) 25 | } 26 | 27 | type Queryer interface { 28 | URLQuery() url.Values 29 | } 30 | 31 | type QuerySettings struct { 32 | NestedFormat NestedQueryFormat 33 | ArrayFormat ArrayQueryFormat 34 | } 35 | 36 | type NestedQueryFormat int 37 | 38 | const ( 39 | NestedQueryFormatBrackets NestedQueryFormat = iota 40 | NestedQueryFormatDots 41 | ) 42 | 43 | type ArrayQueryFormat int 44 | 45 | const ( 46 | ArrayQueryFormatComma ArrayQueryFormat = iota 47 | ArrayQueryFormatRepeat 48 | ArrayQueryFormatIndices 49 | ArrayQueryFormatBrackets 50 | ) 51 | -------------------------------------------------------------------------------- /internal/apiquery/query_test.go: -------------------------------------------------------------------------------- 1 | package apiquery 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func P[T any](v T) *T { return &v } 10 | 11 | type Primitives struct { 12 | A bool `query:"a"` 13 | B int `query:"b"` 14 | C uint `query:"c"` 15 | D float64 `query:"d"` 16 | E float32 `query:"e"` 17 | F []int `query:"f"` 18 | } 19 | 20 | type PrimitivePointers struct { 21 | A *bool `query:"a"` 22 | B *int `query:"b"` 23 | C *uint `query:"c"` 24 | D *float64 `query:"d"` 25 | E *float32 `query:"e"` 26 | F *[]int `query:"f"` 27 | } 28 | 29 | type Slices struct { 30 | Slice []Primitives `query:"slices"` 31 | Mixed []interface{} `query:"mixed"` 32 | } 33 | 34 | type DateTime struct { 35 | Date time.Time `query:"date" format:"date"` 36 | DateTime time.Time `query:"date-time" format:"date-time"` 37 | } 38 | 39 | type AdditionalProperties struct { 40 | A bool `query:"a"` 41 | Extras map[string]interface{} `query:"-,inline"` 42 | } 43 | 44 | type Recursive struct { 45 | Name string `query:"name"` 46 | Child *Recursive `query:"child"` 47 | } 48 | 49 | type UnknownStruct struct { 50 | Unknown interface{} `query:"unknown"` 51 | } 52 | 53 | type UnionStruct struct { 54 | Union Union `query:"union" format:"date"` 55 | } 56 | 57 | type Union interface { 58 | union() 59 | } 60 | 61 | type UnionInteger int64 62 | 63 | func (UnionInteger) union() {} 64 | 65 | type UnionString string 66 | 67 | func (UnionString) union() {} 68 | 69 | type UnionStructA struct { 70 | Type string `query:"type"` 71 | A string `query:"a"` 72 | B string `query:"b"` 73 | } 74 | 75 | func (UnionStructA) union() {} 76 | 77 | type UnionStructB struct { 78 | Type string `query:"type"` 79 | A string `query:"a"` 80 | } 81 | 82 | func (UnionStructB) union() {} 83 | 84 | type UnionTime time.Time 85 | 86 | func (UnionTime) union() {} 87 | 88 | type DeeplyNested struct { 89 | A DeeplyNested1 `query:"a"` 90 | } 91 | 92 | type DeeplyNested1 struct { 93 | B DeeplyNested2 `query:"b"` 94 | } 95 | 96 | type DeeplyNested2 struct { 97 | C DeeplyNested3 `query:"c"` 98 | } 99 | 100 | type DeeplyNested3 struct { 101 | D *string `query:"d"` 102 | } 103 | 104 | var tests = map[string]struct { 105 | enc string 106 | val interface{} 107 | settings QuerySettings 108 | }{ 109 | "primitives": { 110 | "a=false&b=237628372683&c=654&d=9999.43&e=43.7599983215332&f=1,2,3,4", 111 | Primitives{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}, 112 | QuerySettings{}, 113 | }, 114 | 115 | "slices_brackets": { 116 | `mixed[]=1&mixed[]=2.3&mixed[]=hello&slices[][a]=false&slices[][a]=false&slices[][b]=237628372683&slices[][b]=237628372683&slices[][c]=654&slices[][c]=654&slices[][d]=9999.43&slices[][d]=9999.43&slices[][e]=43.7599983215332&slices[][e]=43.7599983215332&slices[][f][]=1&slices[][f][]=2&slices[][f][]=3&slices[][f][]=4&slices[][f][]=1&slices[][f][]=2&slices[][f][]=3&slices[][f][]=4`, 117 | Slices{ 118 | Slice: []Primitives{ 119 | {A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}, 120 | {A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}, 121 | }, 122 | Mixed: []interface{}{1, 2.3, "hello"}, 123 | }, 124 | QuerySettings{ArrayFormat: ArrayQueryFormatBrackets}, 125 | }, 126 | 127 | "slices_comma": { 128 | `mixed=1,2.3,hello`, 129 | Slices{ 130 | Mixed: []interface{}{1, 2.3, "hello"}, 131 | }, 132 | QuerySettings{ArrayFormat: ArrayQueryFormatComma}, 133 | }, 134 | 135 | "slices_repeat": { 136 | `mixed=1&mixed=2.3&mixed=hello&slices[a]=false&slices[a]=false&slices[b]=237628372683&slices[b]=237628372683&slices[c]=654&slices[c]=654&slices[d]=9999.43&slices[d]=9999.43&slices[e]=43.7599983215332&slices[e]=43.7599983215332&slices[f]=1&slices[f]=2&slices[f]=3&slices[f]=4&slices[f]=1&slices[f]=2&slices[f]=3&slices[f]=4`, 137 | Slices{ 138 | Slice: []Primitives{ 139 | {A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}, 140 | {A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}, 141 | }, 142 | Mixed: []interface{}{1, 2.3, "hello"}, 143 | }, 144 | QuerySettings{ArrayFormat: ArrayQueryFormatRepeat}, 145 | }, 146 | 147 | "primitive_pointer_struct": { 148 | "a=false&b=237628372683&c=654&d=9999.43&e=43.7599983215332&f=1,2,3,4,5", 149 | PrimitivePointers{ 150 | A: P(false), 151 | B: P(237628372683), 152 | C: P(uint(654)), 153 | D: P(9999.43), 154 | E: P(float32(43.76)), 155 | F: &[]int{1, 2, 3, 4, 5}, 156 | }, 157 | QuerySettings{}, 158 | }, 159 | 160 | "datetime_struct": { 161 | `date=2006-01-02&date-time=2006-01-02T15:04:05Z`, 162 | DateTime{ 163 | Date: time.Date(2006, time.January, 2, 0, 0, 0, 0, time.UTC), 164 | DateTime: time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC), 165 | }, 166 | QuerySettings{}, 167 | }, 168 | 169 | "additional_properties": { 170 | `a=true&bar=value&foo=true`, 171 | AdditionalProperties{ 172 | A: true, 173 | Extras: map[string]interface{}{ 174 | "bar": "value", 175 | "foo": true, 176 | }, 177 | }, 178 | QuerySettings{}, 179 | }, 180 | 181 | "recursive_struct_brackets": { 182 | `child[name]=Alex&name=Robert`, 183 | Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}}, 184 | QuerySettings{NestedFormat: NestedQueryFormatBrackets}, 185 | }, 186 | 187 | "recursive_struct_dots": { 188 | `child.name=Alex&name=Robert`, 189 | Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}}, 190 | QuerySettings{NestedFormat: NestedQueryFormatDots}, 191 | }, 192 | 193 | "unknown_struct_number": { 194 | `unknown=12`, 195 | UnknownStruct{ 196 | Unknown: 12., 197 | }, 198 | QuerySettings{}, 199 | }, 200 | 201 | "unknown_struct_map_brackets": { 202 | `unknown[foo]=bar`, 203 | UnknownStruct{ 204 | Unknown: map[string]interface{}{ 205 | "foo": "bar", 206 | }, 207 | }, 208 | QuerySettings{NestedFormat: NestedQueryFormatBrackets}, 209 | }, 210 | 211 | "unknown_struct_map_dots": { 212 | `unknown.foo=bar`, 213 | UnknownStruct{ 214 | Unknown: map[string]interface{}{ 215 | "foo": "bar", 216 | }, 217 | }, 218 | QuerySettings{NestedFormat: NestedQueryFormatDots}, 219 | }, 220 | 221 | "union_string": { 222 | `union=hello`, 223 | UnionStruct{ 224 | Union: UnionString("hello"), 225 | }, 226 | QuerySettings{}, 227 | }, 228 | 229 | "union_integer": { 230 | `union=12`, 231 | UnionStruct{ 232 | Union: UnionInteger(12), 233 | }, 234 | QuerySettings{}, 235 | }, 236 | 237 | "union_struct_discriminated_a": { 238 | `union[a]=foo&union[b]=bar&union[type]=typeA`, 239 | UnionStruct{ 240 | Union: UnionStructA{ 241 | Type: "typeA", 242 | A: "foo", 243 | B: "bar", 244 | }, 245 | }, 246 | QuerySettings{}, 247 | }, 248 | 249 | "union_struct_discriminated_b": { 250 | `union[a]=foo&union[type]=typeB`, 251 | UnionStruct{ 252 | Union: UnionStructB{ 253 | Type: "typeB", 254 | A: "foo", 255 | }, 256 | }, 257 | QuerySettings{}, 258 | }, 259 | 260 | "union_struct_time": { 261 | `union=2010-05-23`, 262 | UnionStruct{ 263 | Union: UnionTime(time.Date(2010, 05, 23, 0, 0, 0, 0, time.UTC)), 264 | }, 265 | QuerySettings{}, 266 | }, 267 | 268 | "deeply_nested_brackets": { 269 | `a[b][c][d]=hello`, 270 | DeeplyNested{ 271 | A: DeeplyNested1{ 272 | B: DeeplyNested2{ 273 | C: DeeplyNested3{ 274 | D: P("hello"), 275 | }, 276 | }, 277 | }, 278 | }, 279 | QuerySettings{NestedFormat: NestedQueryFormatBrackets}, 280 | }, 281 | 282 | "deeply_nested_dots": { 283 | `a.b.c.d=hello`, 284 | DeeplyNested{ 285 | A: DeeplyNested1{ 286 | B: DeeplyNested2{ 287 | C: DeeplyNested3{ 288 | D: P("hello"), 289 | }, 290 | }, 291 | }, 292 | }, 293 | QuerySettings{NestedFormat: NestedQueryFormatDots}, 294 | }, 295 | 296 | "deeply_nested_brackets_empty": { 297 | ``, 298 | DeeplyNested{ 299 | A: DeeplyNested1{ 300 | B: DeeplyNested2{ 301 | C: DeeplyNested3{ 302 | D: nil, 303 | }, 304 | }, 305 | }, 306 | }, 307 | QuerySettings{NestedFormat: NestedQueryFormatBrackets}, 308 | }, 309 | 310 | "deeply_nested_dots_empty": { 311 | ``, 312 | DeeplyNested{ 313 | A: DeeplyNested1{ 314 | B: DeeplyNested2{ 315 | C: DeeplyNested3{ 316 | D: nil, 317 | }, 318 | }, 319 | }, 320 | }, 321 | QuerySettings{NestedFormat: NestedQueryFormatDots}, 322 | }, 323 | } 324 | 325 | func TestEncode(t *testing.T) { 326 | for name, test := range tests { 327 | t.Run(name, func(t *testing.T) { 328 | values := MarshalWithSettings(test.val, test.settings) 329 | str, _ := url.QueryUnescape(values.Encode()) 330 | if str != test.enc { 331 | t.Fatalf("expected %+#v to serialize to %s but got %s", test.val, test.enc, str) 332 | } 333 | }) 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /internal/apiquery/tag.go: -------------------------------------------------------------------------------- 1 | package apiquery 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | ) 7 | 8 | const queryStructTag = "query" 9 | const formatStructTag = "format" 10 | 11 | type parsedStructTag struct { 12 | name string 13 | omitempty bool 14 | inline bool 15 | } 16 | 17 | func parseQueryStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) { 18 | raw, ok := field.Tag.Lookup(queryStructTag) 19 | if !ok { 20 | return 21 | } 22 | parts := strings.Split(raw, ",") 23 | if len(parts) == 0 { 24 | return tag, false 25 | } 26 | tag.name = parts[0] 27 | for _, part := range parts[1:] { 28 | switch part { 29 | case "omitempty": 30 | tag.omitempty = true 31 | case "inline": 32 | tag.inline = true 33 | } 34 | } 35 | return 36 | } 37 | 38 | func parseFormatStructTag(field reflect.StructField) (format string, ok bool) { 39 | format, ok = field.Tag.Lookup(formatStructTag) 40 | return 41 | } 42 | -------------------------------------------------------------------------------- /internal/param/field.go: -------------------------------------------------------------------------------- 1 | package param 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type FieldLike interface{ field() } 8 | 9 | // Field is a wrapper used for all values sent to the API, 10 | // to distinguish zero values from null or omitted fields. 11 | // 12 | // It also allows sending arbitrary deserializable values. 13 | // 14 | // To instantiate a Field, use the helpers exported from 15 | // the package root: `F()`, `Null()`, `Raw()`, etc. 16 | type Field[T any] struct { 17 | FieldLike 18 | Value T 19 | Null bool 20 | Present bool 21 | Raw any 22 | } 23 | 24 | func (f Field[T]) String() string { 25 | if s, ok := any(f.Value).(fmt.Stringer); ok { 26 | return s.String() 27 | } 28 | return fmt.Sprintf("%v", f.Value) 29 | } 30 | -------------------------------------------------------------------------------- /internal/testutil/testutil.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "strconv" 7 | "testing" 8 | ) 9 | 10 | func CheckTestServer(t *testing.T, url string) bool { 11 | if _, err := http.Get(url); err != nil { 12 | const SKIP_MOCK_TESTS = "SKIP_MOCK_TESTS" 13 | if str, ok := os.LookupEnv(SKIP_MOCK_TESTS); ok { 14 | skip, err := strconv.ParseBool(str) 15 | if err != nil { 16 | t.Fatalf("strconv.ParseBool(os.LookupEnv(%s)) failed: %s", SKIP_MOCK_TESTS, err) 17 | } 18 | if skip { 19 | t.Skip("The test will not run without a mock Prism server running against your OpenAPI spec") 20 | return false 21 | } 22 | t.Errorf("The test will not run without a mock Prism server running against your OpenAPI spec. You can set the environment variable %s to true to skip running any tests that require the mock server", SKIP_MOCK_TESTS) 23 | return false 24 | } 25 | } 26 | return true 27 | } 28 | -------------------------------------------------------------------------------- /internal/version.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package internal 4 | 5 | const PackageVersion = "1.16.3" // x-release-please-version 6 | -------------------------------------------------------------------------------- /lib/.keep: -------------------------------------------------------------------------------- 1 | File generated from our OpenAPI spec by Stainless. 2 | 3 | This directory can be used to store custom files to expand the SDK. 4 | It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. -------------------------------------------------------------------------------- /option/middleware.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package option 4 | 5 | import ( 6 | "log" 7 | "net/http" 8 | "net/http/httputil" 9 | ) 10 | 11 | // WithDebugLog logs the HTTP request and response content. 12 | // If the logger parameter is nil, it uses the default logger. 13 | // 14 | // WithDebugLog is for debugging and development purposes only. 15 | // It should not be used in production code. The behavior and interface 16 | // of WithDebugLog is not guaranteed to be stable. 17 | func WithDebugLog(logger *log.Logger) RequestOption { 18 | return WithMiddleware(func(req *http.Request, nxt MiddlewareNext) (*http.Response, error) { 19 | if logger == nil { 20 | logger = log.Default() 21 | } 22 | 23 | if reqBytes, err := httputil.DumpRequest(req, true); err == nil { 24 | logger.Printf("Request Content:\n%s\n", reqBytes) 25 | } 26 | 27 | resp, err := nxt(req) 28 | if err != nil { 29 | return resp, err 30 | } 31 | 32 | if respBytes, err := httputil.DumpResponse(resp, true); err == nil { 33 | logger.Printf("Response Content:\n%s\n", respBytes) 34 | } 35 | 36 | return resp, err 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /option/requestoption.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package option 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | "strings" 12 | "time" 13 | 14 | "github.com/terminaldotshop/terminal-sdk-go/internal/requestconfig" 15 | "github.com/tidwall/sjson" 16 | ) 17 | 18 | // RequestOption is an option for the requests made by the terminal API Client 19 | // which can be supplied to clients, services, and methods. You can read more about this functional 20 | // options pattern in our [README]. 21 | // 22 | // [README]: https://pkg.go.dev/github.com/terminaldotshop/terminal-sdk-go#readme-requestoptions 23 | type RequestOption = requestconfig.RequestOption 24 | 25 | // WithBaseURL returns a RequestOption that sets the BaseURL for the client. 26 | // 27 | // For security reasons, ensure that the base URL is trusted. 28 | func WithBaseURL(base string) RequestOption { 29 | u, err := url.Parse(base) 30 | if err == nil && u.Path != "" && !strings.HasSuffix(u.Path, "/") { 31 | u.Path += "/" 32 | } 33 | 34 | return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { 35 | if err != nil { 36 | return fmt.Errorf("requestoption: WithBaseURL failed to parse url %s", err) 37 | } 38 | 39 | r.BaseURL = u 40 | return nil 41 | }) 42 | } 43 | 44 | // HTTPClient is primarily used to describe an [*http.Client], but also 45 | // supports custom implementations. 46 | // 47 | // For bespoke implementations, prefer using an [*http.Client] with a 48 | // custom transport. See [http.RoundTripper] for further information. 49 | type HTTPClient interface { 50 | Do(*http.Request) (*http.Response, error) 51 | } 52 | 53 | // WithHTTPClient returns a RequestOption that changes the underlying http client used to make this 54 | // request, which by default is [http.DefaultClient]. 55 | // 56 | // For custom uses cases, it is recommended to provide an [*http.Client] with a custom 57 | // [http.RoundTripper] as its transport, rather than directly implementing [HTTPClient]. 58 | func WithHTTPClient(client HTTPClient) RequestOption { 59 | return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { 60 | if client == nil { 61 | return fmt.Errorf("requestoption: custom http client cannot be nil") 62 | } 63 | 64 | if c, ok := client.(*http.Client); ok { 65 | // Prefer the native client if possible. 66 | r.HTTPClient = c 67 | r.CustomHTTPDoer = nil 68 | } else { 69 | r.CustomHTTPDoer = client 70 | } 71 | 72 | return nil 73 | }) 74 | } 75 | 76 | // MiddlewareNext is a function which is called by a middleware to pass an HTTP request 77 | // to the next stage in the middleware chain. 78 | type MiddlewareNext = func(*http.Request) (*http.Response, error) 79 | 80 | // Middleware is a function which intercepts HTTP requests, processing or modifying 81 | // them, and then passing the request to the next middleware or handler 82 | // in the chain by calling the provided MiddlewareNext function. 83 | type Middleware = func(*http.Request, MiddlewareNext) (*http.Response, error) 84 | 85 | // WithMiddleware returns a RequestOption that applies the given middleware 86 | // to the requests made. Each middleware will execute in the order they were given. 87 | func WithMiddleware(middlewares ...Middleware) RequestOption { 88 | return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { 89 | r.Middlewares = append(r.Middlewares, middlewares...) 90 | return nil 91 | }) 92 | } 93 | 94 | // WithMaxRetries returns a RequestOption that sets the maximum number of retries that the client 95 | // attempts to make. When given 0, the client only makes one request. By 96 | // default, the client retries two times. 97 | // 98 | // WithMaxRetries panics when retries is negative. 99 | func WithMaxRetries(retries int) RequestOption { 100 | if retries < 0 { 101 | panic("option: cannot have fewer than 0 retries") 102 | } 103 | return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { 104 | r.MaxRetries = retries 105 | return nil 106 | }) 107 | } 108 | 109 | // WithHeader returns a RequestOption that sets the header value to the associated key. It overwrites 110 | // any value if there was one already present. 111 | func WithHeader(key, value string) RequestOption { 112 | return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { 113 | r.Request.Header.Set(key, value) 114 | return nil 115 | }) 116 | } 117 | 118 | // WithHeaderAdd returns a RequestOption that adds the header value to the associated key. It appends 119 | // onto any existing values. 120 | func WithHeaderAdd(key, value string) RequestOption { 121 | return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { 122 | r.Request.Header.Add(key, value) 123 | return nil 124 | }) 125 | } 126 | 127 | // WithHeaderDel returns a RequestOption that deletes the header value(s) associated with the given key. 128 | func WithHeaderDel(key string) RequestOption { 129 | return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { 130 | r.Request.Header.Del(key) 131 | return nil 132 | }) 133 | } 134 | 135 | // WithQuery returns a RequestOption that sets the query value to the associated key. It overwrites 136 | // any value if there was one already present. 137 | func WithQuery(key, value string) RequestOption { 138 | return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { 139 | query := r.Request.URL.Query() 140 | query.Set(key, value) 141 | r.Request.URL.RawQuery = query.Encode() 142 | return nil 143 | }) 144 | } 145 | 146 | // WithQueryAdd returns a RequestOption that adds the query value to the associated key. It appends 147 | // onto any existing values. 148 | func WithQueryAdd(key, value string) RequestOption { 149 | return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { 150 | query := r.Request.URL.Query() 151 | query.Add(key, value) 152 | r.Request.URL.RawQuery = query.Encode() 153 | return nil 154 | }) 155 | } 156 | 157 | // WithQueryDel returns a RequestOption that deletes the query value(s) associated with the key. 158 | func WithQueryDel(key string) RequestOption { 159 | return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { 160 | query := r.Request.URL.Query() 161 | query.Del(key) 162 | r.Request.URL.RawQuery = query.Encode() 163 | return nil 164 | }) 165 | } 166 | 167 | // WithJSONSet returns a RequestOption that sets the body's JSON value associated with the key. 168 | // The key accepts a string as defined by the [sjson format]. 169 | // 170 | // [sjson format]: https://github.com/tidwall/sjson 171 | func WithJSONSet(key string, value interface{}) RequestOption { 172 | return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) (err error) { 173 | var b []byte 174 | 175 | if r.Body == nil { 176 | b, err = sjson.SetBytes(nil, key, value) 177 | if err != nil { 178 | return err 179 | } 180 | } else if buffer, ok := r.Body.(*bytes.Buffer); ok { 181 | b = buffer.Bytes() 182 | b, err = sjson.SetBytes(b, key, value) 183 | if err != nil { 184 | return err 185 | } 186 | } else { 187 | return fmt.Errorf("cannot use WithJSONSet on a body that is not serialized as *bytes.Buffer") 188 | } 189 | 190 | r.Body = bytes.NewBuffer(b) 191 | return nil 192 | }) 193 | } 194 | 195 | // WithJSONDel returns a RequestOption that deletes the body's JSON value associated with the key. 196 | // The key accepts a string as defined by the [sjson format]. 197 | // 198 | // [sjson format]: https://github.com/tidwall/sjson 199 | func WithJSONDel(key string) RequestOption { 200 | return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) (err error) { 201 | if buffer, ok := r.Body.(*bytes.Buffer); ok { 202 | b := buffer.Bytes() 203 | b, err = sjson.DeleteBytes(b, key) 204 | if err != nil { 205 | return err 206 | } 207 | r.Body = bytes.NewBuffer(b) 208 | return nil 209 | } 210 | 211 | return fmt.Errorf("cannot use WithJSONDel on a body that is not serialized as *bytes.Buffer") 212 | }) 213 | } 214 | 215 | // WithResponseBodyInto returns a RequestOption that overwrites the deserialization target with 216 | // the given destination. If provided, we don't deserialize into the default struct. 217 | func WithResponseBodyInto(dst any) RequestOption { 218 | return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { 219 | r.ResponseBodyInto = dst 220 | return nil 221 | }) 222 | } 223 | 224 | // WithResponseInto returns a RequestOption that copies the [*http.Response] into the given address. 225 | func WithResponseInto(dst **http.Response) RequestOption { 226 | return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { 227 | r.ResponseInto = dst 228 | return nil 229 | }) 230 | } 231 | 232 | // WithRequestBody returns a RequestOption that provides a custom serialized body with the given 233 | // content type. 234 | // 235 | // body accepts an io.Reader or raw []bytes. 236 | func WithRequestBody(contentType string, body any) RequestOption { 237 | return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { 238 | if reader, ok := body.(io.Reader); ok { 239 | r.Body = reader 240 | return r.Apply(WithHeader("Content-Type", contentType)) 241 | } 242 | 243 | if b, ok := body.([]byte); ok { 244 | r.Body = bytes.NewBuffer(b) 245 | return r.Apply(WithHeader("Content-Type", contentType)) 246 | } 247 | 248 | return fmt.Errorf("body must be a byte slice or implement io.Reader") 249 | }) 250 | } 251 | 252 | // WithRequestTimeout returns a RequestOption that sets the timeout for 253 | // each request attempt. This should be smaller than the timeout defined in 254 | // the context, which spans all retries. 255 | func WithRequestTimeout(dur time.Duration) RequestOption { 256 | return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { 257 | r.RequestTimeout = dur 258 | return nil 259 | }) 260 | } 261 | 262 | // WithEnvironmentProduction returns a RequestOption that sets the current 263 | // environment to be the "production" environment. An environment specifies which base URL 264 | // to use by default. 265 | func WithEnvironmentProduction() RequestOption { 266 | return requestconfig.WithDefaultBaseURL("https://api.terminal.shop/") 267 | } 268 | 269 | // WithEnvironmentDev returns a RequestOption that sets the current 270 | // environment to be the "dev" environment. An environment specifies which base URL 271 | // to use by default. 272 | func WithEnvironmentDev() RequestOption { 273 | return requestconfig.WithDefaultBaseURL("https://api.dev.terminal.shop/") 274 | } 275 | 276 | // WithBearerToken returns a RequestOption that sets the client setting "bearer_token". 277 | func WithBearerToken(value string) RequestOption { 278 | return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { 279 | r.BearerToken = value 280 | return r.Apply(WithHeader("authorization", fmt.Sprintf("Bearer %s", r.BearerToken))) 281 | }) 282 | } 283 | 284 | // WithAppID returns a RequestOption that sets the client setting "app_id". 285 | func WithAppID(value string) RequestOption { 286 | return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { 287 | r.AppID = value 288 | return r.Apply(WithHeader("x-terminal-app-id", value)) 289 | }) 290 | } 291 | -------------------------------------------------------------------------------- /order.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package githubcomterminaldotshopterminalsdkgo 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "fmt" 9 | "net/http" 10 | "slices" 11 | 12 | "github.com/terminaldotshop/terminal-sdk-go/internal/apijson" 13 | "github.com/terminaldotshop/terminal-sdk-go/internal/param" 14 | "github.com/terminaldotshop/terminal-sdk-go/internal/requestconfig" 15 | "github.com/terminaldotshop/terminal-sdk-go/option" 16 | ) 17 | 18 | // OrderService contains methods and other services that help with interacting with 19 | // the terminal API. 20 | // 21 | // Note, unlike clients, this service does not read variables from the environment 22 | // automatically. You should not instantiate this service directly, and instead use 23 | // the [NewOrderService] method instead. 24 | type OrderService struct { 25 | Options []option.RequestOption 26 | } 27 | 28 | // NewOrderService generates a new service that applies the given options to each 29 | // request. These options are applied after the parent client's options (if there 30 | // is one), and before any request-specific options. 31 | func NewOrderService(opts ...option.RequestOption) (r *OrderService) { 32 | r = &OrderService{} 33 | r.Options = opts 34 | return 35 | } 36 | 37 | // Create an order without a cart. The order will be placed immediately. 38 | func (r *OrderService) New(ctx context.Context, body OrderNewParams, opts ...option.RequestOption) (res *OrderNewResponse, err error) { 39 | opts = slices.Concat(r.Options, opts) 40 | path := "order" 41 | err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) 42 | return 43 | } 44 | 45 | // List the orders associated with the current user. 46 | func (r *OrderService) List(ctx context.Context, opts ...option.RequestOption) (res *OrderListResponse, err error) { 47 | opts = slices.Concat(r.Options, opts) 48 | path := "order" 49 | err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) 50 | return 51 | } 52 | 53 | // Get the order with the given ID. 54 | func (r *OrderService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *OrderGetResponse, err error) { 55 | opts = slices.Concat(r.Options, opts) 56 | if id == "" { 57 | err = errors.New("missing required id parameter") 58 | return 59 | } 60 | path := fmt.Sprintf("order/%s", id) 61 | err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) 62 | return 63 | } 64 | 65 | // An order from the Terminal shop. 66 | type Order struct { 67 | // Unique object identifier. The format and length of IDs may change over time. 68 | ID string `json:"id,required"` 69 | // The subtotal and shipping amounts of the order. 70 | Amount OrderAmount `json:"amount,required"` 71 | // Date the order was created. 72 | Created string `json:"created,required"` 73 | // Items in the order. 74 | Items []OrderItem `json:"items,required"` 75 | // Shipping address of the order. 76 | Shipping OrderShipping `json:"shipping,required"` 77 | // Tracking information of the order. 78 | Tracking OrderTracking `json:"tracking,required"` 79 | // Zero-based index of the order for this user only. 80 | Index int64 `json:"index"` 81 | JSON orderJSON `json:"-"` 82 | } 83 | 84 | // orderJSON contains the JSON metadata for the struct [Order] 85 | type orderJSON struct { 86 | ID apijson.Field 87 | Amount apijson.Field 88 | Created apijson.Field 89 | Items apijson.Field 90 | Shipping apijson.Field 91 | Tracking apijson.Field 92 | Index apijson.Field 93 | raw string 94 | ExtraFields map[string]apijson.Field 95 | } 96 | 97 | func (r *Order) UnmarshalJSON(data []byte) (err error) { 98 | return apijson.UnmarshalRoot(data, r) 99 | } 100 | 101 | func (r orderJSON) RawJSON() string { 102 | return r.raw 103 | } 104 | 105 | // The subtotal and shipping amounts of the order. 106 | type OrderAmount struct { 107 | // Shipping amount of the order, in cents (USD). 108 | Shipping int64 `json:"shipping,required"` 109 | // Subtotal amount of the order, in cents (USD). 110 | Subtotal int64 `json:"subtotal,required"` 111 | JSON orderAmountJSON `json:"-"` 112 | } 113 | 114 | // orderAmountJSON contains the JSON metadata for the struct [OrderAmount] 115 | type orderAmountJSON struct { 116 | Shipping apijson.Field 117 | Subtotal apijson.Field 118 | raw string 119 | ExtraFields map[string]apijson.Field 120 | } 121 | 122 | func (r *OrderAmount) UnmarshalJSON(data []byte) (err error) { 123 | return apijson.UnmarshalRoot(data, r) 124 | } 125 | 126 | func (r orderAmountJSON) RawJSON() string { 127 | return r.raw 128 | } 129 | 130 | type OrderItem struct { 131 | // Unique object identifier. The format and length of IDs may change over time. 132 | ID string `json:"id,required"` 133 | // Amount of the item in the order, in cents (USD). 134 | Amount int64 `json:"amount,required"` 135 | // Quantity of the item in the order. 136 | Quantity int64 `json:"quantity,required"` 137 | // Description of the item in the order. 138 | Description string `json:"description"` 139 | // ID of the product variant of the item in the order. 140 | ProductVariantID string `json:"productVariantID"` 141 | JSON orderItemJSON `json:"-"` 142 | } 143 | 144 | // orderItemJSON contains the JSON metadata for the struct [OrderItem] 145 | type orderItemJSON struct { 146 | ID apijson.Field 147 | Amount apijson.Field 148 | Quantity apijson.Field 149 | Description apijson.Field 150 | ProductVariantID apijson.Field 151 | raw string 152 | ExtraFields map[string]apijson.Field 153 | } 154 | 155 | func (r *OrderItem) UnmarshalJSON(data []byte) (err error) { 156 | return apijson.UnmarshalRoot(data, r) 157 | } 158 | 159 | func (r orderItemJSON) RawJSON() string { 160 | return r.raw 161 | } 162 | 163 | // Shipping address of the order. 164 | type OrderShipping struct { 165 | // City of the address. 166 | City string `json:"city,required"` 167 | // ISO 3166-1 alpha-2 country code of the address. 168 | Country string `json:"country,required"` 169 | // The recipient's name. 170 | Name string `json:"name,required"` 171 | // Street of the address. 172 | Street1 string `json:"street1,required"` 173 | // Zip code of the address. 174 | Zip string `json:"zip,required"` 175 | // Phone number of the recipient. 176 | Phone string `json:"phone"` 177 | // Province or state of the address. 178 | Province string `json:"province"` 179 | // Apartment, suite, etc. of the address. 180 | Street2 string `json:"street2"` 181 | JSON orderShippingJSON `json:"-"` 182 | } 183 | 184 | // orderShippingJSON contains the JSON metadata for the struct [OrderShipping] 185 | type orderShippingJSON struct { 186 | City apijson.Field 187 | Country apijson.Field 188 | Name apijson.Field 189 | Street1 apijson.Field 190 | Zip apijson.Field 191 | Phone apijson.Field 192 | Province apijson.Field 193 | Street2 apijson.Field 194 | raw string 195 | ExtraFields map[string]apijson.Field 196 | } 197 | 198 | func (r *OrderShipping) UnmarshalJSON(data []byte) (err error) { 199 | return apijson.UnmarshalRoot(data, r) 200 | } 201 | 202 | func (r orderShippingJSON) RawJSON() string { 203 | return r.raw 204 | } 205 | 206 | // Tracking information of the order. 207 | type OrderTracking struct { 208 | // Tracking number of the order. 209 | Number string `json:"number"` 210 | // Shipping service of the order. 211 | Service string `json:"service"` 212 | // Current tracking status of the shipment. 213 | Status OrderTrackingStatus `json:"status"` 214 | // Additional details about the tracking status. 215 | StatusDetails string `json:"statusDetails"` 216 | // When the tracking status was last updated. 217 | StatusUpdatedAt string `json:"statusUpdatedAt"` 218 | // Tracking URL of the order. 219 | URL string `json:"url"` 220 | JSON orderTrackingJSON `json:"-"` 221 | } 222 | 223 | // orderTrackingJSON contains the JSON metadata for the struct [OrderTracking] 224 | type orderTrackingJSON struct { 225 | Number apijson.Field 226 | Service apijson.Field 227 | Status apijson.Field 228 | StatusDetails apijson.Field 229 | StatusUpdatedAt apijson.Field 230 | URL apijson.Field 231 | raw string 232 | ExtraFields map[string]apijson.Field 233 | } 234 | 235 | func (r *OrderTracking) UnmarshalJSON(data []byte) (err error) { 236 | return apijson.UnmarshalRoot(data, r) 237 | } 238 | 239 | func (r orderTrackingJSON) RawJSON() string { 240 | return r.raw 241 | } 242 | 243 | // Current tracking status of the shipment. 244 | type OrderTrackingStatus string 245 | 246 | const ( 247 | OrderTrackingStatusPreTransit OrderTrackingStatus = "PRE_TRANSIT" 248 | OrderTrackingStatusTransit OrderTrackingStatus = "TRANSIT" 249 | OrderTrackingStatusDelivered OrderTrackingStatus = "DELIVERED" 250 | OrderTrackingStatusReturned OrderTrackingStatus = "RETURNED" 251 | OrderTrackingStatusFailure OrderTrackingStatus = "FAILURE" 252 | OrderTrackingStatusUnknown OrderTrackingStatus = "UNKNOWN" 253 | ) 254 | 255 | func (r OrderTrackingStatus) IsKnown() bool { 256 | switch r { 257 | case OrderTrackingStatusPreTransit, OrderTrackingStatusTransit, OrderTrackingStatusDelivered, OrderTrackingStatusReturned, OrderTrackingStatusFailure, OrderTrackingStatusUnknown: 258 | return true 259 | } 260 | return false 261 | } 262 | 263 | type OrderNewResponse struct { 264 | // Order ID. 265 | Data string `json:"data,required"` 266 | JSON orderNewResponseJSON `json:"-"` 267 | } 268 | 269 | // orderNewResponseJSON contains the JSON metadata for the struct 270 | // [OrderNewResponse] 271 | type orderNewResponseJSON struct { 272 | Data apijson.Field 273 | raw string 274 | ExtraFields map[string]apijson.Field 275 | } 276 | 277 | func (r *OrderNewResponse) UnmarshalJSON(data []byte) (err error) { 278 | return apijson.UnmarshalRoot(data, r) 279 | } 280 | 281 | func (r orderNewResponseJSON) RawJSON() string { 282 | return r.raw 283 | } 284 | 285 | type OrderListResponse struct { 286 | // List of orders. 287 | Data []Order `json:"data,required"` 288 | JSON orderListResponseJSON `json:"-"` 289 | } 290 | 291 | // orderListResponseJSON contains the JSON metadata for the struct 292 | // [OrderListResponse] 293 | type orderListResponseJSON struct { 294 | Data apijson.Field 295 | raw string 296 | ExtraFields map[string]apijson.Field 297 | } 298 | 299 | func (r *OrderListResponse) UnmarshalJSON(data []byte) (err error) { 300 | return apijson.UnmarshalRoot(data, r) 301 | } 302 | 303 | func (r orderListResponseJSON) RawJSON() string { 304 | return r.raw 305 | } 306 | 307 | type OrderGetResponse struct { 308 | // An order from the Terminal shop. 309 | Data Order `json:"data,required"` 310 | JSON orderGetResponseJSON `json:"-"` 311 | } 312 | 313 | // orderGetResponseJSON contains the JSON metadata for the struct 314 | // [OrderGetResponse] 315 | type orderGetResponseJSON struct { 316 | Data apijson.Field 317 | raw string 318 | ExtraFields map[string]apijson.Field 319 | } 320 | 321 | func (r *OrderGetResponse) UnmarshalJSON(data []byte) (err error) { 322 | return apijson.UnmarshalRoot(data, r) 323 | } 324 | 325 | func (r orderGetResponseJSON) RawJSON() string { 326 | return r.raw 327 | } 328 | 329 | type OrderNewParams struct { 330 | // Shipping address ID. 331 | AddressID param.Field[string] `json:"addressID,required"` 332 | // Card ID. 333 | CardID param.Field[string] `json:"cardID,required"` 334 | // Product variants to include in the order, along with their quantities. 335 | Variants param.Field[map[string]int64] `json:"variants,required"` 336 | } 337 | 338 | func (r OrderNewParams) MarshalJSON() (data []byte, err error) { 339 | return apijson.MarshalRoot(r) 340 | } 341 | -------------------------------------------------------------------------------- /order_test.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package githubcomterminaldotshopterminalsdkgo_test 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "os" 9 | "testing" 10 | 11 | "github.com/terminaldotshop/terminal-sdk-go" 12 | "github.com/terminaldotshop/terminal-sdk-go/internal/testutil" 13 | "github.com/terminaldotshop/terminal-sdk-go/option" 14 | ) 15 | 16 | func TestOrderNew(t *testing.T) { 17 | baseURL := "http://localhost:4010" 18 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 19 | baseURL = envURL 20 | } 21 | if !testutil.CheckTestServer(t, baseURL) { 22 | return 23 | } 24 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 25 | option.WithBaseURL(baseURL), 26 | option.WithBearerToken("My Bearer Token"), 27 | ) 28 | _, err := client.Order.New(context.TODO(), githubcomterminaldotshopterminalsdkgo.OrderNewParams{ 29 | AddressID: githubcomterminaldotshopterminalsdkgo.F("shp_XXXXXXXXXXXXXXXXXXXXXXXXX"), 30 | CardID: githubcomterminaldotshopterminalsdkgo.F("crd_XXXXXXXXXXXXXXXXXXXXXXXXX"), 31 | Variants: githubcomterminaldotshopterminalsdkgo.F(map[string]int64{ 32 | "var_XXXXXXXXXXXXXXXXXXXXXXXXX": int64(1), 33 | }), 34 | }) 35 | if err != nil { 36 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 37 | if errors.As(err, &apierr) { 38 | t.Log(string(apierr.DumpRequest(true))) 39 | } 40 | t.Fatalf("err should be nil: %s", err.Error()) 41 | } 42 | } 43 | 44 | func TestOrderList(t *testing.T) { 45 | baseURL := "http://localhost:4010" 46 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 47 | baseURL = envURL 48 | } 49 | if !testutil.CheckTestServer(t, baseURL) { 50 | return 51 | } 52 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 53 | option.WithBaseURL(baseURL), 54 | option.WithBearerToken("My Bearer Token"), 55 | ) 56 | _, err := client.Order.List(context.TODO()) 57 | if err != nil { 58 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 59 | if errors.As(err, &apierr) { 60 | t.Log(string(apierr.DumpRequest(true))) 61 | } 62 | t.Fatalf("err should be nil: %s", err.Error()) 63 | } 64 | } 65 | 66 | func TestOrderGet(t *testing.T) { 67 | baseURL := "http://localhost:4010" 68 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 69 | baseURL = envURL 70 | } 71 | if !testutil.CheckTestServer(t, baseURL) { 72 | return 73 | } 74 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 75 | option.WithBaseURL(baseURL), 76 | option.WithBearerToken("My Bearer Token"), 77 | ) 78 | _, err := client.Order.Get(context.TODO(), "ord_XXXXXXXXXXXXXXXXXXXXXXXXX") 79 | if err != nil { 80 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 81 | if errors.As(err, &apierr) { 82 | t.Log(string(apierr.DumpRequest(true))) 83 | } 84 | t.Fatalf("err should be nil: %s", err.Error()) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /product.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package githubcomterminaldotshopterminalsdkgo 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "fmt" 9 | "net/http" 10 | "slices" 11 | 12 | "github.com/terminaldotshop/terminal-sdk-go/internal/apijson" 13 | "github.com/terminaldotshop/terminal-sdk-go/internal/requestconfig" 14 | "github.com/terminaldotshop/terminal-sdk-go/option" 15 | ) 16 | 17 | // ProductService contains methods and other services that help with interacting 18 | // with the terminal API. 19 | // 20 | // Note, unlike clients, this service does not read variables from the environment 21 | // automatically. You should not instantiate this service directly, and instead use 22 | // the [NewProductService] method instead. 23 | type ProductService struct { 24 | Options []option.RequestOption 25 | } 26 | 27 | // NewProductService generates a new service that applies the given options to each 28 | // request. These options are applied after the parent client's options (if there 29 | // is one), and before any request-specific options. 30 | func NewProductService(opts ...option.RequestOption) (r *ProductService) { 31 | r = &ProductService{} 32 | r.Options = opts 33 | return 34 | } 35 | 36 | // List all products for sale in the Terminal shop. 37 | func (r *ProductService) List(ctx context.Context, opts ...option.RequestOption) (res *ProductListResponse, err error) { 38 | opts = slices.Concat(r.Options, opts) 39 | path := "product" 40 | err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) 41 | return 42 | } 43 | 44 | // Get a product by ID from the Terminal shop. 45 | func (r *ProductService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *ProductGetResponse, err error) { 46 | opts = slices.Concat(r.Options, opts) 47 | if id == "" { 48 | err = errors.New("missing required id parameter") 49 | return 50 | } 51 | path := fmt.Sprintf("product/%s", id) 52 | err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) 53 | return 54 | } 55 | 56 | // Product sold in the Terminal shop. 57 | type Product struct { 58 | // Unique object identifier. The format and length of IDs may change over time. 59 | ID string `json:"id,required"` 60 | // Description of the product. 61 | Description string `json:"description,required"` 62 | // Name of the product. 63 | Name string `json:"name,required"` 64 | // List of variants of the product. 65 | Variants []ProductVariant `json:"variants,required"` 66 | // Order of the product used when displaying a sorted list of products. 67 | Order int64 `json:"order"` 68 | // Whether the product must be or can be subscribed to. 69 | Subscription ProductSubscription `json:"subscription"` 70 | // Tags for the product. 71 | Tags ProductTags `json:"tags"` 72 | // Timestamp when the product was hidden from public view. 73 | TimeHidden string `json:"timeHidden"` 74 | JSON productJSON `json:"-"` 75 | } 76 | 77 | // productJSON contains the JSON metadata for the struct [Product] 78 | type productJSON struct { 79 | ID apijson.Field 80 | Description apijson.Field 81 | Name apijson.Field 82 | Variants apijson.Field 83 | Order apijson.Field 84 | Subscription apijson.Field 85 | Tags apijson.Field 86 | TimeHidden apijson.Field 87 | raw string 88 | ExtraFields map[string]apijson.Field 89 | } 90 | 91 | func (r *Product) UnmarshalJSON(data []byte) (err error) { 92 | return apijson.UnmarshalRoot(data, r) 93 | } 94 | 95 | func (r productJSON) RawJSON() string { 96 | return r.raw 97 | } 98 | 99 | // Whether the product must be or can be subscribed to. 100 | type ProductSubscription string 101 | 102 | const ( 103 | ProductSubscriptionAllowed ProductSubscription = "allowed" 104 | ProductSubscriptionRequired ProductSubscription = "required" 105 | ) 106 | 107 | func (r ProductSubscription) IsKnown() bool { 108 | switch r { 109 | case ProductSubscriptionAllowed, ProductSubscriptionRequired: 110 | return true 111 | } 112 | return false 113 | } 114 | 115 | // Tags for the product. 116 | type ProductTags struct { 117 | App string `json:"app"` 118 | Color string `json:"color"` 119 | Featured bool `json:"featured"` 120 | MarketEu bool `json:"market_eu"` 121 | MarketGlobal bool `json:"market_global"` 122 | MarketNa bool `json:"market_na"` 123 | JSON productTagsJSON `json:"-"` 124 | } 125 | 126 | // productTagsJSON contains the JSON metadata for the struct [ProductTags] 127 | type productTagsJSON struct { 128 | App apijson.Field 129 | Color apijson.Field 130 | Featured apijson.Field 131 | MarketEu apijson.Field 132 | MarketGlobal apijson.Field 133 | MarketNa apijson.Field 134 | raw string 135 | ExtraFields map[string]apijson.Field 136 | } 137 | 138 | func (r *ProductTags) UnmarshalJSON(data []byte) (err error) { 139 | return apijson.UnmarshalRoot(data, r) 140 | } 141 | 142 | func (r productTagsJSON) RawJSON() string { 143 | return r.raw 144 | } 145 | 146 | // Variant of a product in the Terminal shop. 147 | type ProductVariant struct { 148 | // Unique object identifier. The format and length of IDs may change over time. 149 | ID string `json:"id,required"` 150 | // Name of the product variant. 151 | Name string `json:"name,required"` 152 | // Price of the product variant in cents (USD). 153 | Price int64 `json:"price,required"` 154 | // Tags for the product variant. 155 | Tags ProductVariantTags `json:"tags"` 156 | JSON productVariantJSON `json:"-"` 157 | } 158 | 159 | // productVariantJSON contains the JSON metadata for the struct [ProductVariant] 160 | type productVariantJSON struct { 161 | ID apijson.Field 162 | Name apijson.Field 163 | Price apijson.Field 164 | Tags apijson.Field 165 | raw string 166 | ExtraFields map[string]apijson.Field 167 | } 168 | 169 | func (r *ProductVariant) UnmarshalJSON(data []byte) (err error) { 170 | return apijson.UnmarshalRoot(data, r) 171 | } 172 | 173 | func (r productVariantJSON) RawJSON() string { 174 | return r.raw 175 | } 176 | 177 | // Tags for the product variant. 178 | type ProductVariantTags struct { 179 | App string `json:"app"` 180 | MarketEu bool `json:"market_eu"` 181 | MarketGlobal bool `json:"market_global"` 182 | MarketNa bool `json:"market_na"` 183 | JSON productVariantTagsJSON `json:"-"` 184 | } 185 | 186 | // productVariantTagsJSON contains the JSON metadata for the struct 187 | // [ProductVariantTags] 188 | type productVariantTagsJSON struct { 189 | App apijson.Field 190 | MarketEu apijson.Field 191 | MarketGlobal apijson.Field 192 | MarketNa apijson.Field 193 | raw string 194 | ExtraFields map[string]apijson.Field 195 | } 196 | 197 | func (r *ProductVariantTags) UnmarshalJSON(data []byte) (err error) { 198 | return apijson.UnmarshalRoot(data, r) 199 | } 200 | 201 | func (r productVariantTagsJSON) RawJSON() string { 202 | return r.raw 203 | } 204 | 205 | type ProductListResponse struct { 206 | // A list of products. 207 | Data []Product `json:"data,required"` 208 | JSON productListResponseJSON `json:"-"` 209 | } 210 | 211 | // productListResponseJSON contains the JSON metadata for the struct 212 | // [ProductListResponse] 213 | type productListResponseJSON struct { 214 | Data apijson.Field 215 | raw string 216 | ExtraFields map[string]apijson.Field 217 | } 218 | 219 | func (r *ProductListResponse) UnmarshalJSON(data []byte) (err error) { 220 | return apijson.UnmarshalRoot(data, r) 221 | } 222 | 223 | func (r productListResponseJSON) RawJSON() string { 224 | return r.raw 225 | } 226 | 227 | type ProductGetResponse struct { 228 | // Product sold in the Terminal shop. 229 | Data Product `json:"data,required"` 230 | JSON productGetResponseJSON `json:"-"` 231 | } 232 | 233 | // productGetResponseJSON contains the JSON metadata for the struct 234 | // [ProductGetResponse] 235 | type productGetResponseJSON struct { 236 | Data apijson.Field 237 | raw string 238 | ExtraFields map[string]apijson.Field 239 | } 240 | 241 | func (r *ProductGetResponse) UnmarshalJSON(data []byte) (err error) { 242 | return apijson.UnmarshalRoot(data, r) 243 | } 244 | 245 | func (r productGetResponseJSON) RawJSON() string { 246 | return r.raw 247 | } 248 | -------------------------------------------------------------------------------- /product_test.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package githubcomterminaldotshopterminalsdkgo_test 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "os" 9 | "testing" 10 | 11 | "github.com/terminaldotshop/terminal-sdk-go" 12 | "github.com/terminaldotshop/terminal-sdk-go/internal/testutil" 13 | "github.com/terminaldotshop/terminal-sdk-go/option" 14 | ) 15 | 16 | func TestProductList(t *testing.T) { 17 | baseURL := "http://localhost:4010" 18 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 19 | baseURL = envURL 20 | } 21 | if !testutil.CheckTestServer(t, baseURL) { 22 | return 23 | } 24 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 25 | option.WithBaseURL(baseURL), 26 | option.WithBearerToken("My Bearer Token"), 27 | ) 28 | _, err := client.Product.List(context.TODO()) 29 | if err != nil { 30 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 31 | if errors.As(err, &apierr) { 32 | t.Log(string(apierr.DumpRequest(true))) 33 | } 34 | t.Fatalf("err should be nil: %s", err.Error()) 35 | } 36 | } 37 | 38 | func TestProductGet(t *testing.T) { 39 | baseURL := "http://localhost:4010" 40 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 41 | baseURL = envURL 42 | } 43 | if !testutil.CheckTestServer(t, baseURL) { 44 | return 45 | } 46 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 47 | option.WithBaseURL(baseURL), 48 | option.WithBearerToken("My Bearer Token"), 49 | ) 50 | _, err := client.Product.Get(context.TODO(), "prd_XXXXXXXXXXXXXXXXXXXXXXXXX") 51 | if err != nil { 52 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 53 | if errors.As(err, &apierr) { 54 | t.Log(string(apierr.DumpRequest(true))) 55 | } 56 | t.Fatalf("err should be nil: %s", err.Error()) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /profile.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package githubcomterminaldotshopterminalsdkgo 4 | 5 | import ( 6 | "context" 7 | "net/http" 8 | "slices" 9 | 10 | "github.com/terminaldotshop/terminal-sdk-go/internal/apijson" 11 | "github.com/terminaldotshop/terminal-sdk-go/internal/param" 12 | "github.com/terminaldotshop/terminal-sdk-go/internal/requestconfig" 13 | "github.com/terminaldotshop/terminal-sdk-go/option" 14 | ) 15 | 16 | // ProfileService contains methods and other services that help with interacting 17 | // with the terminal API. 18 | // 19 | // Note, unlike clients, this service does not read variables from the environment 20 | // automatically. You should not instantiate this service directly, and instead use 21 | // the [NewProfileService] method instead. 22 | type ProfileService struct { 23 | Options []option.RequestOption 24 | } 25 | 26 | // NewProfileService generates a new service that applies the given options to each 27 | // request. These options are applied after the parent client's options (if there 28 | // is one), and before any request-specific options. 29 | func NewProfileService(opts ...option.RequestOption) (r *ProfileService) { 30 | r = &ProfileService{} 31 | r.Options = opts 32 | return 33 | } 34 | 35 | // Update the current user's profile. 36 | func (r *ProfileService) Update(ctx context.Context, body ProfileUpdateParams, opts ...option.RequestOption) (res *ProfileUpdateResponse, err error) { 37 | opts = slices.Concat(r.Options, opts) 38 | path := "profile" 39 | err = requestconfig.ExecuteNewRequest(ctx, http.MethodPut, path, body, &res, opts...) 40 | return 41 | } 42 | 43 | // Get the current user's profile. 44 | func (r *ProfileService) Me(ctx context.Context, opts ...option.RequestOption) (res *ProfileMeResponse, err error) { 45 | opts = slices.Concat(r.Options, opts) 46 | path := "profile" 47 | err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) 48 | return 49 | } 50 | 51 | // A Terminal shop user's profile. (We have users, btw.) 52 | type Profile struct { 53 | // A Terminal shop user. (We have users, btw.) 54 | User ProfileUser `json:"user,required"` 55 | JSON profileJSON `json:"-"` 56 | } 57 | 58 | // profileJSON contains the JSON metadata for the struct [Profile] 59 | type profileJSON struct { 60 | User apijson.Field 61 | raw string 62 | ExtraFields map[string]apijson.Field 63 | } 64 | 65 | func (r *Profile) UnmarshalJSON(data []byte) (err error) { 66 | return apijson.UnmarshalRoot(data, r) 67 | } 68 | 69 | func (r profileJSON) RawJSON() string { 70 | return r.raw 71 | } 72 | 73 | // A Terminal shop user. (We have users, btw.) 74 | type ProfileUser struct { 75 | // Unique object identifier. The format and length of IDs may change over time. 76 | ID string `json:"id,required"` 77 | // Email address of the user. 78 | Email string `json:"email,required,nullable"` 79 | // The user's fingerprint, derived from their public SSH key. 80 | Fingerprint string `json:"fingerprint,required,nullable"` 81 | // Name of the user. 82 | Name string `json:"name,required,nullable"` 83 | // Stripe customer ID of the user. 84 | StripeCustomerID string `json:"stripeCustomerID,required"` 85 | JSON profileUserJSON `json:"-"` 86 | } 87 | 88 | // profileUserJSON contains the JSON metadata for the struct [ProfileUser] 89 | type profileUserJSON struct { 90 | ID apijson.Field 91 | Email apijson.Field 92 | Fingerprint apijson.Field 93 | Name apijson.Field 94 | StripeCustomerID apijson.Field 95 | raw string 96 | ExtraFields map[string]apijson.Field 97 | } 98 | 99 | func (r *ProfileUser) UnmarshalJSON(data []byte) (err error) { 100 | return apijson.UnmarshalRoot(data, r) 101 | } 102 | 103 | func (r profileUserJSON) RawJSON() string { 104 | return r.raw 105 | } 106 | 107 | type ProfileUpdateResponse struct { 108 | // A Terminal shop user's profile. (We have users, btw.) 109 | Data Profile `json:"data,required"` 110 | JSON profileUpdateResponseJSON `json:"-"` 111 | } 112 | 113 | // profileUpdateResponseJSON contains the JSON metadata for the struct 114 | // [ProfileUpdateResponse] 115 | type profileUpdateResponseJSON struct { 116 | Data apijson.Field 117 | raw string 118 | ExtraFields map[string]apijson.Field 119 | } 120 | 121 | func (r *ProfileUpdateResponse) UnmarshalJSON(data []byte) (err error) { 122 | return apijson.UnmarshalRoot(data, r) 123 | } 124 | 125 | func (r profileUpdateResponseJSON) RawJSON() string { 126 | return r.raw 127 | } 128 | 129 | type ProfileMeResponse struct { 130 | // A Terminal shop user's profile. (We have users, btw.) 131 | Data Profile `json:"data,required"` 132 | JSON profileMeResponseJSON `json:"-"` 133 | } 134 | 135 | // profileMeResponseJSON contains the JSON metadata for the struct 136 | // [ProfileMeResponse] 137 | type profileMeResponseJSON struct { 138 | Data apijson.Field 139 | raw string 140 | ExtraFields map[string]apijson.Field 141 | } 142 | 143 | func (r *ProfileMeResponse) UnmarshalJSON(data []byte) (err error) { 144 | return apijson.UnmarshalRoot(data, r) 145 | } 146 | 147 | func (r profileMeResponseJSON) RawJSON() string { 148 | return r.raw 149 | } 150 | 151 | type ProfileUpdateParams struct { 152 | Email param.Field[string] `json:"email,required" format:"email"` 153 | Name param.Field[string] `json:"name,required"` 154 | } 155 | 156 | func (r ProfileUpdateParams) MarshalJSON() (data []byte, err error) { 157 | return apijson.MarshalRoot(r) 158 | } 159 | -------------------------------------------------------------------------------- /profile_test.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package githubcomterminaldotshopterminalsdkgo_test 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "os" 9 | "testing" 10 | 11 | "github.com/terminaldotshop/terminal-sdk-go" 12 | "github.com/terminaldotshop/terminal-sdk-go/internal/testutil" 13 | "github.com/terminaldotshop/terminal-sdk-go/option" 14 | ) 15 | 16 | func TestProfileUpdate(t *testing.T) { 17 | baseURL := "http://localhost:4010" 18 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 19 | baseURL = envURL 20 | } 21 | if !testutil.CheckTestServer(t, baseURL) { 22 | return 23 | } 24 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 25 | option.WithBaseURL(baseURL), 26 | option.WithBearerToken("My Bearer Token"), 27 | ) 28 | _, err := client.Profile.Update(context.TODO(), githubcomterminaldotshopterminalsdkgo.ProfileUpdateParams{ 29 | Email: githubcomterminaldotshopterminalsdkgo.F("john@example.com"), 30 | Name: githubcomterminaldotshopterminalsdkgo.F("John Doe"), 31 | }) 32 | if err != nil { 33 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 34 | if errors.As(err, &apierr) { 35 | t.Log(string(apierr.DumpRequest(true))) 36 | } 37 | t.Fatalf("err should be nil: %s", err.Error()) 38 | } 39 | } 40 | 41 | func TestProfileMe(t *testing.T) { 42 | baseURL := "http://localhost:4010" 43 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 44 | baseURL = envURL 45 | } 46 | if !testutil.CheckTestServer(t, baseURL) { 47 | return 48 | } 49 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 50 | option.WithBaseURL(baseURL), 51 | option.WithBearerToken("My Bearer Token"), 52 | ) 53 | _, err := client.Profile.Me(context.TODO()) 54 | if err != nil { 55 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 56 | if errors.As(err, &apierr) { 57 | t.Log(string(apierr.DumpRequest(true))) 58 | } 59 | t.Fatalf("err should be nil: %s", err.Error()) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | ".": {} 4 | }, 5 | "$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json", 6 | "include-v-in-tag": true, 7 | "include-component-in-tag": false, 8 | "versioning": "prerelease", 9 | "prerelease": true, 10 | "bump-minor-pre-major": true, 11 | "bump-patch-for-minor-pre-major": false, 12 | "pull-request-header": "Automated Release PR", 13 | "pull-request-title-pattern": "release: ${version}", 14 | "changelog-sections": [ 15 | { 16 | "type": "feat", 17 | "section": "Features" 18 | }, 19 | { 20 | "type": "fix", 21 | "section": "Bug Fixes" 22 | }, 23 | { 24 | "type": "perf", 25 | "section": "Performance Improvements" 26 | }, 27 | { 28 | "type": "revert", 29 | "section": "Reverts" 30 | }, 31 | { 32 | "type": "chore", 33 | "section": "Chores" 34 | }, 35 | { 36 | "type": "docs", 37 | "section": "Documentation" 38 | }, 39 | { 40 | "type": "style", 41 | "section": "Styles" 42 | }, 43 | { 44 | "type": "refactor", 45 | "section": "Refactors" 46 | }, 47 | { 48 | "type": "test", 49 | "section": "Tests", 50 | "hidden": true 51 | }, 52 | { 53 | "type": "build", 54 | "section": "Build System" 55 | }, 56 | { 57 | "type": "ci", 58 | "section": "Continuous Integration", 59 | "hidden": true 60 | } 61 | ], 62 | "release-type": "go", 63 | "extra-files": [ 64 | "internal/version.go", 65 | "README.md" 66 | ] 67 | } -------------------------------------------------------------------------------- /scripts/bootstrap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then 8 | brew bundle check >/dev/null 2>&1 || { 9 | echo -n "==> Install Homebrew dependencies? (y/N): " 10 | read -r response 11 | case "$response" in 12 | [yY][eE][sS]|[yY]) 13 | brew bundle 14 | ;; 15 | *) 16 | ;; 17 | esac 18 | echo 19 | } 20 | fi 21 | 22 | echo "==> Installing Go dependencies…" 23 | 24 | go mod tidy -e 25 | -------------------------------------------------------------------------------- /scripts/format: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | echo "==> Running gofmt -s -w" 8 | gofmt -s -w . 9 | -------------------------------------------------------------------------------- /scripts/lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | echo "==> Running Go build" 8 | go build ./... 9 | 10 | echo "==> Checking tests compile" 11 | go test -run=^$ ./... 12 | -------------------------------------------------------------------------------- /scripts/mock: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | if [[ -n "$1" && "$1" != '--'* ]]; then 8 | URL="$1" 9 | shift 10 | else 11 | URL="$(grep 'openapi_spec_url' .stats.yml | cut -d' ' -f2)" 12 | fi 13 | 14 | # Check if the URL is empty 15 | if [ -z "$URL" ]; then 16 | echo "Error: No OpenAPI spec path/url provided or found in .stats.yml" 17 | exit 1 18 | fi 19 | 20 | echo "==> Starting mock server with URL ${URL}" 21 | 22 | # Run prism mock on the given spec 23 | if [ "$1" == "--daemon" ]; then 24 | npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & 25 | 26 | # Wait for server to come online 27 | echo -n "Waiting for server" 28 | while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do 29 | echo -n "." 30 | sleep 0.1 31 | done 32 | 33 | if grep -q "✖ fatal" ".prism.log"; then 34 | cat .prism.log 35 | exit 1 36 | fi 37 | 38 | echo 39 | else 40 | npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" 41 | fi 42 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | RED='\033[0;31m' 8 | GREEN='\033[0;32m' 9 | YELLOW='\033[0;33m' 10 | NC='\033[0m' # No Color 11 | 12 | function prism_is_running() { 13 | curl --silent "http://localhost:4010" >/dev/null 2>&1 14 | } 15 | 16 | kill_server_on_port() { 17 | pids=$(lsof -t -i tcp:"$1" || echo "") 18 | if [ "$pids" != "" ]; then 19 | kill "$pids" 20 | echo "Stopped $pids." 21 | fi 22 | } 23 | 24 | function is_overriding_api_base_url() { 25 | [ -n "$TEST_API_BASE_URL" ] 26 | } 27 | 28 | if ! is_overriding_api_base_url && ! prism_is_running ; then 29 | # When we exit this script, make sure to kill the background mock server process 30 | trap 'kill_server_on_port 4010' EXIT 31 | 32 | # Start the dev server 33 | ./scripts/mock --daemon 34 | fi 35 | 36 | if is_overriding_api_base_url ; then 37 | echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" 38 | echo 39 | elif ! prism_is_running ; then 40 | echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" 41 | echo -e "running against your OpenAPI spec." 42 | echo 43 | echo -e "To run the server, pass in the path or url of your OpenAPI" 44 | echo -e "spec to the prism command:" 45 | echo 46 | echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" 47 | echo 48 | 49 | exit 1 50 | else 51 | echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" 52 | echo 53 | fi 54 | 55 | echo "==> Running tests" 56 | go test ./... "$@" 57 | -------------------------------------------------------------------------------- /subscription_test.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package githubcomterminaldotshopterminalsdkgo_test 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "os" 9 | "testing" 10 | 11 | "github.com/terminaldotshop/terminal-sdk-go" 12 | "github.com/terminaldotshop/terminal-sdk-go/internal/testutil" 13 | "github.com/terminaldotshop/terminal-sdk-go/option" 14 | ) 15 | 16 | func TestSubscriptionNewWithOptionalParams(t *testing.T) { 17 | baseURL := "http://localhost:4010" 18 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 19 | baseURL = envURL 20 | } 21 | if !testutil.CheckTestServer(t, baseURL) { 22 | return 23 | } 24 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 25 | option.WithBaseURL(baseURL), 26 | option.WithBearerToken("My Bearer Token"), 27 | ) 28 | _, err := client.Subscription.New(context.TODO(), githubcomterminaldotshopterminalsdkgo.SubscriptionNewParams{ 29 | Subscription: githubcomterminaldotshopterminalsdkgo.SubscriptionParam{ 30 | ID: githubcomterminaldotshopterminalsdkgo.F("sub_XXXXXXXXXXXXXXXXXXXXXXXXX"), 31 | AddressID: githubcomterminaldotshopterminalsdkgo.F("shp_XXXXXXXXXXXXXXXXXXXXXXXXX"), 32 | CardID: githubcomterminaldotshopterminalsdkgo.F("crd_XXXXXXXXXXXXXXXXXXXXXXXXX"), 33 | Created: githubcomterminaldotshopterminalsdkgo.F("2024-06-29T19:36:19.000Z"), 34 | Price: githubcomterminaldotshopterminalsdkgo.F(int64(2200)), 35 | ProductVariantID: githubcomterminaldotshopterminalsdkgo.F("var_XXXXXXXXXXXXXXXXXXXXXXXXX"), 36 | Quantity: githubcomterminaldotshopterminalsdkgo.F(int64(1)), 37 | Next: githubcomterminaldotshopterminalsdkgo.F("2025-02-01T19:36:19.000Z"), 38 | Schedule: githubcomterminaldotshopterminalsdkgo.F[githubcomterminaldotshopterminalsdkgo.SubscriptionScheduleUnionParam](githubcomterminaldotshopterminalsdkgo.SubscriptionScheduleWeeklyParam{ 39 | Interval: githubcomterminaldotshopterminalsdkgo.F(int64(3)), 40 | Type: githubcomterminaldotshopterminalsdkgo.F(githubcomterminaldotshopterminalsdkgo.SubscriptionScheduleWeeklyTypeWeekly), 41 | }), 42 | }, 43 | }) 44 | if err != nil { 45 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 46 | if errors.As(err, &apierr) { 47 | t.Log(string(apierr.DumpRequest(true))) 48 | } 49 | t.Fatalf("err should be nil: %s", err.Error()) 50 | } 51 | } 52 | 53 | func TestSubscriptionUpdateWithOptionalParams(t *testing.T) { 54 | baseURL := "http://localhost:4010" 55 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 56 | baseURL = envURL 57 | } 58 | if !testutil.CheckTestServer(t, baseURL) { 59 | return 60 | } 61 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 62 | option.WithBaseURL(baseURL), 63 | option.WithBearerToken("My Bearer Token"), 64 | ) 65 | _, err := client.Subscription.Update( 66 | context.TODO(), 67 | "sub_XXXXXXXXXXXXXXXXXXXXXXXXX", 68 | githubcomterminaldotshopterminalsdkgo.SubscriptionUpdateParams{ 69 | AddressID: githubcomterminaldotshopterminalsdkgo.F("shp_XXXXXXXXXXXXXXXXXXXXXXXXX"), 70 | CardID: githubcomterminaldotshopterminalsdkgo.F("crd_XXXXXXXXXXXXXXXXXXXXXXXXX"), 71 | Schedule: githubcomterminaldotshopterminalsdkgo.F[githubcomterminaldotshopterminalsdkgo.SubscriptionUpdateParamsScheduleUnion](githubcomterminaldotshopterminalsdkgo.SubscriptionUpdateParamsScheduleWeekly{ 72 | Interval: githubcomterminaldotshopterminalsdkgo.F(int64(3)), 73 | Type: githubcomterminaldotshopterminalsdkgo.F(githubcomterminaldotshopterminalsdkgo.SubscriptionUpdateParamsScheduleWeeklyTypeWeekly), 74 | }), 75 | }, 76 | ) 77 | if err != nil { 78 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 79 | if errors.As(err, &apierr) { 80 | t.Log(string(apierr.DumpRequest(true))) 81 | } 82 | t.Fatalf("err should be nil: %s", err.Error()) 83 | } 84 | } 85 | 86 | func TestSubscriptionList(t *testing.T) { 87 | baseURL := "http://localhost:4010" 88 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 89 | baseURL = envURL 90 | } 91 | if !testutil.CheckTestServer(t, baseURL) { 92 | return 93 | } 94 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 95 | option.WithBaseURL(baseURL), 96 | option.WithBearerToken("My Bearer Token"), 97 | ) 98 | _, err := client.Subscription.List(context.TODO()) 99 | if err != nil { 100 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 101 | if errors.As(err, &apierr) { 102 | t.Log(string(apierr.DumpRequest(true))) 103 | } 104 | t.Fatalf("err should be nil: %s", err.Error()) 105 | } 106 | } 107 | 108 | func TestSubscriptionDelete(t *testing.T) { 109 | baseURL := "http://localhost:4010" 110 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 111 | baseURL = envURL 112 | } 113 | if !testutil.CheckTestServer(t, baseURL) { 114 | return 115 | } 116 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 117 | option.WithBaseURL(baseURL), 118 | option.WithBearerToken("My Bearer Token"), 119 | ) 120 | _, err := client.Subscription.Delete(context.TODO(), "sub_XXXXXXXXXXXXXXXXXXXXXXXXX") 121 | if err != nil { 122 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 123 | if errors.As(err, &apierr) { 124 | t.Log(string(apierr.DumpRequest(true))) 125 | } 126 | t.Fatalf("err should be nil: %s", err.Error()) 127 | } 128 | } 129 | 130 | func TestSubscriptionGet(t *testing.T) { 131 | baseURL := "http://localhost:4010" 132 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 133 | baseURL = envURL 134 | } 135 | if !testutil.CheckTestServer(t, baseURL) { 136 | return 137 | } 138 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 139 | option.WithBaseURL(baseURL), 140 | option.WithBearerToken("My Bearer Token"), 141 | ) 142 | _, err := client.Subscription.Get(context.TODO(), "sub_XXXXXXXXXXXXXXXXXXXXXXXXX") 143 | if err != nil { 144 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 145 | if errors.As(err, &apierr) { 146 | t.Log(string(apierr.DumpRequest(true))) 147 | } 148 | t.Fatalf("err should be nil: %s", err.Error()) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /token.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package githubcomterminaldotshopterminalsdkgo 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "fmt" 9 | "net/http" 10 | "slices" 11 | 12 | "github.com/terminaldotshop/terminal-sdk-go/internal/apijson" 13 | "github.com/terminaldotshop/terminal-sdk-go/internal/requestconfig" 14 | "github.com/terminaldotshop/terminal-sdk-go/option" 15 | ) 16 | 17 | // TokenService contains methods and other services that help with interacting with 18 | // the terminal API. 19 | // 20 | // Note, unlike clients, this service does not read variables from the environment 21 | // automatically. You should not instantiate this service directly, and instead use 22 | // the [NewTokenService] method instead. 23 | type TokenService struct { 24 | Options []option.RequestOption 25 | } 26 | 27 | // NewTokenService generates a new service that applies the given options to each 28 | // request. These options are applied after the parent client's options (if there 29 | // is one), and before any request-specific options. 30 | func NewTokenService(opts ...option.RequestOption) (r *TokenService) { 31 | r = &TokenService{} 32 | r.Options = opts 33 | return 34 | } 35 | 36 | // Create a personal access token. 37 | func (r *TokenService) New(ctx context.Context, opts ...option.RequestOption) (res *TokenNewResponse, err error) { 38 | opts = slices.Concat(r.Options, opts) 39 | path := "token" 40 | err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...) 41 | return 42 | } 43 | 44 | // List the current user's personal access tokens. 45 | func (r *TokenService) List(ctx context.Context, opts ...option.RequestOption) (res *TokenListResponse, err error) { 46 | opts = slices.Concat(r.Options, opts) 47 | path := "token" 48 | err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) 49 | return 50 | } 51 | 52 | // Delete the personal access token with the given ID. 53 | func (r *TokenService) Delete(ctx context.Context, id string, opts ...option.RequestOption) (res *TokenDeleteResponse, err error) { 54 | opts = slices.Concat(r.Options, opts) 55 | if id == "" { 56 | err = errors.New("missing required id parameter") 57 | return 58 | } 59 | path := fmt.Sprintf("token/%s", id) 60 | err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, &res, opts...) 61 | return 62 | } 63 | 64 | // Get the personal access token with the given ID. 65 | func (r *TokenService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *TokenGetResponse, err error) { 66 | opts = slices.Concat(r.Options, opts) 67 | if id == "" { 68 | err = errors.New("missing required id parameter") 69 | return 70 | } 71 | path := fmt.Sprintf("token/%s", id) 72 | err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) 73 | return 74 | } 75 | 76 | // A personal access token used to access the Terminal API. If you leak this, 77 | // expect large sums of coffee to be ordered on your credit card. 78 | type Token struct { 79 | // Unique object identifier. The format and length of IDs may change over time. 80 | ID string `json:"id,required"` 81 | // Personal access token (obfuscated). 82 | Token string `json:"token,required"` 83 | // The created time for the token. 84 | Created string `json:"created,required"` 85 | JSON tokenJSON `json:"-"` 86 | } 87 | 88 | // tokenJSON contains the JSON metadata for the struct [Token] 89 | type tokenJSON struct { 90 | ID apijson.Field 91 | Token apijson.Field 92 | Created apijson.Field 93 | raw string 94 | ExtraFields map[string]apijson.Field 95 | } 96 | 97 | func (r *Token) UnmarshalJSON(data []byte) (err error) { 98 | return apijson.UnmarshalRoot(data, r) 99 | } 100 | 101 | func (r tokenJSON) RawJSON() string { 102 | return r.raw 103 | } 104 | 105 | type TokenNewResponse struct { 106 | Data TokenNewResponseData `json:"data,required"` 107 | JSON tokenNewResponseJSON `json:"-"` 108 | } 109 | 110 | // tokenNewResponseJSON contains the JSON metadata for the struct 111 | // [TokenNewResponse] 112 | type tokenNewResponseJSON struct { 113 | Data apijson.Field 114 | raw string 115 | ExtraFields map[string]apijson.Field 116 | } 117 | 118 | func (r *TokenNewResponse) UnmarshalJSON(data []byte) (err error) { 119 | return apijson.UnmarshalRoot(data, r) 120 | } 121 | 122 | func (r tokenNewResponseJSON) RawJSON() string { 123 | return r.raw 124 | } 125 | 126 | type TokenNewResponseData struct { 127 | // Personal token ID. 128 | ID string `json:"id,required"` 129 | // Personal access token. Include this in the Authorization header 130 | // (`Bearer `) when accessing the Terminal API. 131 | Token string `json:"token,required"` 132 | JSON tokenNewResponseDataJSON `json:"-"` 133 | } 134 | 135 | // tokenNewResponseDataJSON contains the JSON metadata for the struct 136 | // [TokenNewResponseData] 137 | type tokenNewResponseDataJSON struct { 138 | ID apijson.Field 139 | Token apijson.Field 140 | raw string 141 | ExtraFields map[string]apijson.Field 142 | } 143 | 144 | func (r *TokenNewResponseData) UnmarshalJSON(data []byte) (err error) { 145 | return apijson.UnmarshalRoot(data, r) 146 | } 147 | 148 | func (r tokenNewResponseDataJSON) RawJSON() string { 149 | return r.raw 150 | } 151 | 152 | type TokenListResponse struct { 153 | // List of personal access tokens. 154 | Data []Token `json:"data,required"` 155 | JSON tokenListResponseJSON `json:"-"` 156 | } 157 | 158 | // tokenListResponseJSON contains the JSON metadata for the struct 159 | // [TokenListResponse] 160 | type tokenListResponseJSON struct { 161 | Data apijson.Field 162 | raw string 163 | ExtraFields map[string]apijson.Field 164 | } 165 | 166 | func (r *TokenListResponse) UnmarshalJSON(data []byte) (err error) { 167 | return apijson.UnmarshalRoot(data, r) 168 | } 169 | 170 | func (r tokenListResponseJSON) RawJSON() string { 171 | return r.raw 172 | } 173 | 174 | type TokenDeleteResponse struct { 175 | Data TokenDeleteResponseData `json:"data,required"` 176 | JSON tokenDeleteResponseJSON `json:"-"` 177 | } 178 | 179 | // tokenDeleteResponseJSON contains the JSON metadata for the struct 180 | // [TokenDeleteResponse] 181 | type tokenDeleteResponseJSON struct { 182 | Data apijson.Field 183 | raw string 184 | ExtraFields map[string]apijson.Field 185 | } 186 | 187 | func (r *TokenDeleteResponse) UnmarshalJSON(data []byte) (err error) { 188 | return apijson.UnmarshalRoot(data, r) 189 | } 190 | 191 | func (r tokenDeleteResponseJSON) RawJSON() string { 192 | return r.raw 193 | } 194 | 195 | type TokenDeleteResponseData string 196 | 197 | const ( 198 | TokenDeleteResponseDataOk TokenDeleteResponseData = "ok" 199 | ) 200 | 201 | func (r TokenDeleteResponseData) IsKnown() bool { 202 | switch r { 203 | case TokenDeleteResponseDataOk: 204 | return true 205 | } 206 | return false 207 | } 208 | 209 | type TokenGetResponse struct { 210 | // A personal access token used to access the Terminal API. If you leak this, 211 | // expect large sums of coffee to be ordered on your credit card. 212 | Data Token `json:"data,required"` 213 | JSON tokenGetResponseJSON `json:"-"` 214 | } 215 | 216 | // tokenGetResponseJSON contains the JSON metadata for the struct 217 | // [TokenGetResponse] 218 | type tokenGetResponseJSON struct { 219 | Data apijson.Field 220 | raw string 221 | ExtraFields map[string]apijson.Field 222 | } 223 | 224 | func (r *TokenGetResponse) UnmarshalJSON(data []byte) (err error) { 225 | return apijson.UnmarshalRoot(data, r) 226 | } 227 | 228 | func (r tokenGetResponseJSON) RawJSON() string { 229 | return r.raw 230 | } 231 | -------------------------------------------------------------------------------- /token_test.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package githubcomterminaldotshopterminalsdkgo_test 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "os" 9 | "testing" 10 | 11 | "github.com/terminaldotshop/terminal-sdk-go" 12 | "github.com/terminaldotshop/terminal-sdk-go/internal/testutil" 13 | "github.com/terminaldotshop/terminal-sdk-go/option" 14 | ) 15 | 16 | func TestTokenNew(t *testing.T) { 17 | baseURL := "http://localhost:4010" 18 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 19 | baseURL = envURL 20 | } 21 | if !testutil.CheckTestServer(t, baseURL) { 22 | return 23 | } 24 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 25 | option.WithBaseURL(baseURL), 26 | option.WithBearerToken("My Bearer Token"), 27 | ) 28 | _, err := client.Token.New(context.TODO()) 29 | if err != nil { 30 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 31 | if errors.As(err, &apierr) { 32 | t.Log(string(apierr.DumpRequest(true))) 33 | } 34 | t.Fatalf("err should be nil: %s", err.Error()) 35 | } 36 | } 37 | 38 | func TestTokenList(t *testing.T) { 39 | baseURL := "http://localhost:4010" 40 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 41 | baseURL = envURL 42 | } 43 | if !testutil.CheckTestServer(t, baseURL) { 44 | return 45 | } 46 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 47 | option.WithBaseURL(baseURL), 48 | option.WithBearerToken("My Bearer Token"), 49 | ) 50 | _, err := client.Token.List(context.TODO()) 51 | if err != nil { 52 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 53 | if errors.As(err, &apierr) { 54 | t.Log(string(apierr.DumpRequest(true))) 55 | } 56 | t.Fatalf("err should be nil: %s", err.Error()) 57 | } 58 | } 59 | 60 | func TestTokenDelete(t *testing.T) { 61 | baseURL := "http://localhost:4010" 62 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 63 | baseURL = envURL 64 | } 65 | if !testutil.CheckTestServer(t, baseURL) { 66 | return 67 | } 68 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 69 | option.WithBaseURL(baseURL), 70 | option.WithBearerToken("My Bearer Token"), 71 | ) 72 | _, err := client.Token.Delete(context.TODO(), "pat_XXXXXXXXXXXXXXXXXXXXXXXXX") 73 | if err != nil { 74 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 75 | if errors.As(err, &apierr) { 76 | t.Log(string(apierr.DumpRequest(true))) 77 | } 78 | t.Fatalf("err should be nil: %s", err.Error()) 79 | } 80 | } 81 | 82 | func TestTokenGet(t *testing.T) { 83 | baseURL := "http://localhost:4010" 84 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 85 | baseURL = envURL 86 | } 87 | if !testutil.CheckTestServer(t, baseURL) { 88 | return 89 | } 90 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 91 | option.WithBaseURL(baseURL), 92 | option.WithBearerToken("My Bearer Token"), 93 | ) 94 | _, err := client.Token.Get(context.TODO(), "pat_XXXXXXXXXXXXXXXXXXXXXXXXX") 95 | if err != nil { 96 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 97 | if errors.As(err, &apierr) { 98 | t.Log(string(apierr.DumpRequest(true))) 99 | } 100 | t.Fatalf("err should be nil: %s", err.Error()) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /usage_test.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package githubcomterminaldotshopterminalsdkgo_test 4 | 5 | import ( 6 | "context" 7 | "os" 8 | "testing" 9 | 10 | "github.com/terminaldotshop/terminal-sdk-go" 11 | "github.com/terminaldotshop/terminal-sdk-go/internal/testutil" 12 | "github.com/terminaldotshop/terminal-sdk-go/option" 13 | ) 14 | 15 | func TestUsage(t *testing.T) { 16 | baseURL := "http://localhost:4010" 17 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 18 | baseURL = envURL 19 | } 20 | if !testutil.CheckTestServer(t, baseURL) { 21 | return 22 | } 23 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 24 | option.WithBaseURL(baseURL), 25 | option.WithBearerToken("My Bearer Token"), 26 | ) 27 | products, err := client.Product.List(context.TODO()) 28 | if err != nil { 29 | t.Error(err) 30 | return 31 | } 32 | t.Logf("%+v\n", products.Data) 33 | } 34 | -------------------------------------------------------------------------------- /view.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package githubcomterminaldotshopterminalsdkgo 4 | 5 | import ( 6 | "context" 7 | "net/http" 8 | "slices" 9 | 10 | "github.com/terminaldotshop/terminal-sdk-go/internal/apijson" 11 | "github.com/terminaldotshop/terminal-sdk-go/internal/requestconfig" 12 | "github.com/terminaldotshop/terminal-sdk-go/option" 13 | ) 14 | 15 | // ViewService contains methods and other services that help with interacting with 16 | // the terminal API. 17 | // 18 | // Note, unlike clients, this service does not read variables from the environment 19 | // automatically. You should not instantiate this service directly, and instead use 20 | // the [NewViewService] method instead. 21 | type ViewService struct { 22 | Options []option.RequestOption 23 | } 24 | 25 | // NewViewService generates a new service that applies the given options to each 26 | // request. These options are applied after the parent client's options (if there 27 | // is one), and before any request-specific options. 28 | func NewViewService(opts ...option.RequestOption) (r *ViewService) { 29 | r = &ViewService{} 30 | r.Options = opts 31 | return 32 | } 33 | 34 | // Get initial app data, including user, products, cart, addresses, cards, 35 | // subscriptions, and orders. 36 | func (r *ViewService) Init(ctx context.Context, opts ...option.RequestOption) (res *ViewInitResponse, err error) { 37 | opts = slices.Concat(r.Options, opts) 38 | path := "view/init" 39 | err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) 40 | return 41 | } 42 | 43 | // A Terminal shop user's region. 44 | type Region string 45 | 46 | const ( 47 | RegionEu Region = "eu" 48 | RegionNa Region = "na" 49 | RegionGlobal Region = "global" 50 | ) 51 | 52 | func (r Region) IsKnown() bool { 53 | switch r { 54 | case RegionEu, RegionNa, RegionGlobal: 55 | return true 56 | } 57 | return false 58 | } 59 | 60 | type ViewInitResponse struct { 61 | // Initial app data. 62 | Data ViewInitResponseData `json:"data,required"` 63 | JSON viewInitResponseJSON `json:"-"` 64 | } 65 | 66 | // viewInitResponseJSON contains the JSON metadata for the struct 67 | // [ViewInitResponse] 68 | type viewInitResponseJSON struct { 69 | Data apijson.Field 70 | raw string 71 | ExtraFields map[string]apijson.Field 72 | } 73 | 74 | func (r *ViewInitResponse) UnmarshalJSON(data []byte) (err error) { 75 | return apijson.UnmarshalRoot(data, r) 76 | } 77 | 78 | func (r viewInitResponseJSON) RawJSON() string { 79 | return r.raw 80 | } 81 | 82 | // Initial app data. 83 | type ViewInitResponseData struct { 84 | Addresses []Address `json:"addresses,required"` 85 | Apps []App `json:"apps,required"` 86 | Cards []Card `json:"cards,required"` 87 | // The current Terminal shop user's cart. 88 | Cart Cart `json:"cart,required"` 89 | Orders []Order `json:"orders,required"` 90 | Products []Product `json:"products,required"` 91 | // A Terminal shop user's profile. (We have users, btw.) 92 | Profile Profile `json:"profile,required"` 93 | // A Terminal shop user's region. 94 | Region Region `json:"region,required"` 95 | Subscriptions []Subscription `json:"subscriptions,required"` 96 | Tokens []Token `json:"tokens,required"` 97 | JSON viewInitResponseDataJSON `json:"-"` 98 | } 99 | 100 | // viewInitResponseDataJSON contains the JSON metadata for the struct 101 | // [ViewInitResponseData] 102 | type viewInitResponseDataJSON struct { 103 | Addresses apijson.Field 104 | Apps apijson.Field 105 | Cards apijson.Field 106 | Cart apijson.Field 107 | Orders apijson.Field 108 | Products apijson.Field 109 | Profile apijson.Field 110 | Region apijson.Field 111 | Subscriptions apijson.Field 112 | Tokens apijson.Field 113 | raw string 114 | ExtraFields map[string]apijson.Field 115 | } 116 | 117 | func (r *ViewInitResponseData) UnmarshalJSON(data []byte) (err error) { 118 | return apijson.UnmarshalRoot(data, r) 119 | } 120 | 121 | func (r viewInitResponseDataJSON) RawJSON() string { 122 | return r.raw 123 | } 124 | -------------------------------------------------------------------------------- /view_test.go: -------------------------------------------------------------------------------- 1 | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | package githubcomterminaldotshopterminalsdkgo_test 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "os" 9 | "testing" 10 | 11 | "github.com/terminaldotshop/terminal-sdk-go" 12 | "github.com/terminaldotshop/terminal-sdk-go/internal/testutil" 13 | "github.com/terminaldotshop/terminal-sdk-go/option" 14 | ) 15 | 16 | func TestViewInit(t *testing.T) { 17 | baseURL := "http://localhost:4010" 18 | if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 19 | baseURL = envURL 20 | } 21 | if !testutil.CheckTestServer(t, baseURL) { 22 | return 23 | } 24 | client := githubcomterminaldotshopterminalsdkgo.NewClient( 25 | option.WithBaseURL(baseURL), 26 | option.WithBearerToken("My Bearer Token"), 27 | ) 28 | _, err := client.View.Init(context.TODO()) 29 | if err != nil { 30 | var apierr *githubcomterminaldotshopterminalsdkgo.Error 31 | if errors.As(err, &apierr) { 32 | t.Log(string(apierr.DumpRequest(true))) 33 | } 34 | t.Fatalf("err should be nil: %s", err.Error()) 35 | } 36 | } 37 | --------------------------------------------------------------------------------