diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..889ae34 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,7 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/debian +{ + "name": "Development", + "image": "mcr.microsoft.com/devcontainers/go:1.23-bookworm", + "postCreateCommand": "go mod tidy" +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..948f570 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,49 @@ +name: CI +on: + push: + branches-ignore: + - 'generated' + - 'codegen/**' + - 'integrated/**' + - 'stl-preview-head/**' + - 'stl-preview-base/**' + pull_request: + branches-ignore: + - 'stl-preview-head/**' + - 'stl-preview-base/**' + +jobs: + lint: + timeout-minutes: 10 + name: lint + runs-on: ${{ github.repository == 'stainless-sdks/imagekit-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + + steps: + - uses: actions/checkout@v4 + + - name: Setup go + uses: actions/setup-go@v5 + with: + go-version-file: ./go.mod + + - name: Run lints + run: ./scripts/lint + test: + timeout-minutes: 10 + name: test + runs-on: ${{ github.repository == 'stainless-sdks/imagekit-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + steps: + - uses: actions/checkout@v4 + + - name: Setup go + uses: actions/setup-go@v5 + with: + go-version-file: ./go.mod + + - name: Bootstrap + run: ./scripts/bootstrap + + - name: Run tests + run: ./scripts/test diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml deleted file mode 100644 index 0be7846..0000000 --- a/.github/workflows/test-coverage.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: CI - -on: [push, pull_request] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 2 - - uses: actions/setup-go@v2 - with: - go-version: '1.18' - - name: Run coverage - run: go test $(go list ./... | grep -v test) -race -covermode=atomic -coverprofile=coverage.out - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v2 diff --git a/.gitignore b/.gitignore index 66fd13c..c6d0501 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,4 @@ -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ +.prism.log +codegen.log +Brewfile.lock.json +.idea/ diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..3d2ac0b --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.1.0" +} \ No newline at end of file diff --git a/.stats.yml b/.stats.yml new file mode 100644 index 0000000..b0086e9 --- /dev/null +++ b/.stats.yml @@ -0,0 +1,4 @@ +configured_endpoints: 42 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/imagekit-inc%2Fimagekit-bc7c0d27962b30c19c778656988e154b54696819389289f34420a5e5fdfbd3b8.yml +openapi_spec_hash: 1bfde02a63416c036e9545927f727459 +config_hash: b415c06a3b29485af4601beb94ae1aeb diff --git a/Brewfile b/Brewfile new file mode 100644 index 0000000..577e34a --- /dev/null +++ b/Brewfile @@ -0,0 +1 @@ +brew "go" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8fe0684 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,34 @@ +# Changelog + +## 0.1.0 (2025-09-06) + +Full Changelog: [v0.0.1...v0.1.0](https://github.com/imagekit-developer/imagekit-go/compare/v0.0.1...v0.1.0) + +### Features + +* **api:** add BaseWebhookEvent ([49aee77](https://github.com/imagekit-developer/imagekit-go/commit/49aee77cc145d6f45eb44dd5e6fc080c7fe7a001)) +* **api:** extract UpdateFileDetailsRequest to model ([8b5aea9](https://github.com/imagekit-developer/imagekit-go/commit/8b5aea9dde90cbe13cba1d451fb01ad35dfd2e13)) +* **api:** manual updates ([67b5049](https://github.com/imagekit-developer/imagekit-go/commit/67b5049b88e5feb8815ab087d3efffc9173596fc)) +* **api:** manual updates ([5829397](https://github.com/imagekit-developer/imagekit-go/commit/58293972018671d07621207fabde810e368be16e)) +* **api:** manual updates ([cc22fef](https://github.com/imagekit-developer/imagekit-go/commit/cc22fef638f68ff664019f5ba24f4292c7866771)) +* **api:** manual updates ([051d622](https://github.com/imagekit-developer/imagekit-go/commit/051d6221e188479c80197daaf448ce70c7d1248a)) +* **api:** manual updates ([452fcb4](https://github.com/imagekit-developer/imagekit-go/commit/452fcb4070c0141322f5b49180986a10ee07be22)) +* **api:** manual updates ([66bd0b0](https://github.com/imagekit-developer/imagekit-go/commit/66bd0b077f7ef37b59294dd0761648f35c028747)) +* **api:** manual updates ([9a4ab31](https://github.com/imagekit-developer/imagekit-go/commit/9a4ab3117285fd91a09f4b0ef3b868ca5d46ae63)) +* **api:** manual updates ([67adab7](https://github.com/imagekit-developer/imagekit-go/commit/67adab7a0d64f06f0fc9beed70c67cad3b5f1706)) +* **api:** manual updates ([5237fb5](https://github.com/imagekit-developer/imagekit-go/commit/5237fb51c965361f7d96d3dada6726d54cb7349c)) +* **api:** manual updates ([b7169f4](https://github.com/imagekit-developer/imagekit-go/commit/b7169f426e0c8bcb6cebbd9413a5674cdaf79b5b)) +* **api:** manual updates ([bc53c1a](https://github.com/imagekit-developer/imagekit-go/commit/bc53c1a5493d2fbe02f90523d3578e3cd19b8260)) +* **api:** manual updates ([e197563](https://github.com/imagekit-developer/imagekit-go/commit/e1975634e1a07e8276fe79314f91b84d06a4399d)) + + +### Bug Fixes + +* **internal:** unmarshal correctly when there are multiple discriminators ([b87188b](https://github.com/imagekit-developer/imagekit-go/commit/b87188b901ba7422570ed594ee0185fc6e4545b3)) + + +### Chores + +* **internal:** codegen related update ([a9dae33](https://github.com/imagekit-developer/imagekit-go/commit/a9dae337065c0b204dcf4a3a65da0d9abbe1b376)) +* sync repo ([9c87c18](https://github.com/imagekit-developer/imagekit-go/commit/9c87c185f17e8c881e666149f590559512df501f)) +* update SDK settings ([2d43aee](https://github.com/imagekit-developer/imagekit-go/commit/2d43aee37d06353a55a9894a52ed5b4b8300d91b)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f8550b6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,66 @@ +## Setting up the environment + +To set up the repository, run: + +```sh +$ ./scripts/bootstrap +$ ./scripts/lint +``` + +This will install all the required dependencies and build the SDK. + +You can also [install go 1.18+ manually](https://go.dev/doc/install). + +## Modifying/Adding code + +Most of the SDK is generated code. Modifications to code will be persisted between generations, but may +result in merge conflicts between manual patches and changes from the generator. The generator will never +modify the contents of the `lib/` and `examples/` directories. + +## Adding and running examples + +All files in the `examples/` directory are not modified by the generator and can be freely edited or added to. + +```go +# add an example to examples//main.go + +package main + +func main() { + // ... +} +``` + +```sh +$ go run ./examples/ +``` + +## Using the repository from source + +To use a local version of this library from source in another project, edit the `go.mod` with a replace +directive. This can be done through the CLI with the following: + +```sh +$ go mod edit -replace github.com/imagekit-developer/imagekit-go=/path/to/imagekit-go +``` + +## Running tests + +Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. + +```sh +# you will need npm installed +$ npx prism mock path/to/your/openapi.yml +``` + +```sh +$ ./scripts/test +``` + +## Formatting + +This library uses the standard gofmt code formatter: + +```sh +$ ./scripts/format +``` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e7a4d16 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Image Kit + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index dc4e4cc..904b5e6 100644 --- a/README.md +++ b/README.md @@ -1,660 +1,528 @@ -[ImageKit.io](https://imagekit.io) +# Image Kit Go API Library -# ImageKit.io Go SDK -[![CI](https://github.com/imagekit-developer/imagekit-go/workflows/CI/badge.svg)](https://github.com/imagekit-developer/imagekit-go/) -[![codecov](https://codecov.io/gh/imagekit-developer/imagekit-go/branch/dev/graph/badge.svg)](https://codecov.io/gh/imagekit-developer/imagekit-go) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Twitter Follow](https://img.shields.io/twitter/follow/imagekitio?label=Follow&style=social)](https://twitter.com/ImagekitIo) + -Go SDK for [ImageKit](https://imagekit.io/) implements all the backend API, URL-generation, and other utility functions. +Go Reference -ImageKit is complete media storage, optimization, and transformation solution that comes with an [image and video CDN](https://imagekit.io). It can be integrated with your existing infrastructure - storage like AWS S3, web servers, your CDN, and custom domain names, allowing you to deliver optimized images in minutes with minimal code changes. + -All methods except url generation and utility functions return a response with `ResponseMetaData`, which holds the raw response header, HTTP status code, and raw response body. The Response object also contains the `Data` attribute except when the underlying API call is not supposed to return any data such as delete file API. - -Table of contents - - - * [Installation](#installation) - * [Initialization](#initialization) - * [Response Format](#response-format) - * [Error Handling](#error-handling) - * [URL Generation](#url-generation) - * [File Upload](#file-upload) - * [File Management](#file-management) - * [Metadata API](#metadata-api) - * [Utility Functions](#utility-functions) - * [Rate Limits](#rate-limits) - * [Support](#support) - * [Links](#links) - - -## Version Support - -| SDK Version | Go | -|-------------|-----------| -| 1.x | >=1.18 | +The Image Kit Go library provides convenient access to the [Image Kit REST API](https://imagekit.io/docs) +from applications written in Go. +It is generated with [Stainless](https://www.stainless.com/). ## Installation -```bash -go get github.com/imagekit-developer/imagekit-go -``` - -## Initialization + ```go import ( - "github.com/imagekit-developer/imagekit-go" + "github.com/imagekit-developer/imagekit-go" // imported as imagekit ) - -// Using environment variables IMAGEKIT_PRIVATE_KEY, IMAGEKIT_PUBLIC_KEY and IMAGEKIT_ENDPOINT_URL -ik, err := ImageKit.New() - -// Using keys in argument -ik, err := ImageKit.NewFromParams(imagekit.NewParams{ - PrivateKey: privateKey, - PublicKey: publicKey, - UrlEndpoint: urlEndpoint -}) -``` - -## Response Format -Results returned by functions that call backend API(such as media management, metadata, cache APIs) embeds raw response in `ResponseMetaData`, which can be used to get the response HTTP `StatusCode`, `Header`, and `Body`. The JSON response body is parsed to the appropriate SDK type and assigned to the `Data` attribute. - ``` -resp, err := ik.Metadata.FromFile(ctx, fileId) -log.Println(resp.ResponseMetaData.Header, resp.Data.Url) -``` + -Functions that do not get any response body from API do not include the `Data` attribute in the response. In such cases, only `ResponseMetaData` is available. +Or to pin the version: -## Error Handling -ImageKit API returns a non-2xx status code upon error. -SDK defines the following errors in the API package based on the status code returned: + -``` -imagekit-go/api: -400: ErrBadRequest -401: ErrUnauthorized -403: ErrForbidden -404: ErrNotFound -429: ErrTooManyRequests -500, 502, 503, 504: ErrServer -default: "Undefined Error" +```sh +go get -u 'github.com/imagekit-developer/imagekit-go@v0.1.0' ``` -`err` can be tested using `errors.Is` + -``` -if errors.Is(err, api.ErrForbidden) { - log.Println(err.Message) -} -``` +## Requirements -[See full documentation](https://docs.imagekit.io/api-reference/api-introduction) for further detail. +This library requires Go 1.18+. -## URL-generation +## Usage -### 1. Using image path and image hostname or endpoint -This method allows you to create an URL to access a file using the relative file path and the ImageKit URL endpoint (`urlEndpoint`). The file can be an image, video, or any other static file supported by ImageKit. +The full API of this library can be found in [api.md](api.md). + +```go +package main -``` import ( - ikurl "github.com/imagekit-developer/imagekit-go/url" -) + "bytes" + "context" + "fmt" + "io" -url, err := ik.Url(ikurl.UrlParam{ - Path: "/default-image.jpg", - UrlEndpoint: "https://ik.imagekit.io/your_imagekit_id/endpoint/", - Transformations: []map[string]any{ - { - "width": 400, - "height": 300, - "rotation": 90, - }, - }, -}) -``` -This results in a URL like: -``` -https://ik.imagekit.io/your_imagekit_id/endpoint/tr:h-300,w-400:rt-90/default-image.jpg -``` + "github.com/imagekit-developer/imagekit-go" + "github.com/imagekit-developer/imagekit-go/option" +) -### 2. Using full image URL -This method allows you to add transformation parameters to an absolute URL. For example, if you have configured a custom CNAME and have absolute asset URLs in your database or CMS, you will often need this. +func main() { + client := imagekit.NewClient( + option.WithPrivateAPIKey("My Private API Key"), // defaults to os.LookupEnv("IMAGEKIT_PRIVATE_API_KEY") + option.WithPassword("My Password"), // defaults to os.LookupEnv("OPTIONAL_IMAGEKIT_IGNORES_THIS") + ) + response, err := client.Files.Upload(context.TODO(), imagekit.FileUploadParams{ + File: io.Reader(bytes.NewBuffer([]byte("https://www.example.com/public-url.jpg"))), + FileName: "file-name.jpg", + }) + if err != nil { + panic(err.Error()) + } + fmt.Printf("%+v\n", response.VideoCodec) +} ``` -import ( - ikurl "github.com/imagekit-developer/imagekit-go/url" -) -url, err := ik.Url(ikurl.UrlParam{ - Src: "https://ik.imagekit.io/your_imagekit_id/endpoint/default-image.jpg", - Transformations: []map[string]any{ - { - "width": 400, - "height": 300, - "rotation": 90, - }, - }, -}) -``` +### Request fields -This results in a URL like: +The imagekit library uses the [`omitzero`](https://tip.golang.org/doc/go1.24#encodingjsonpkgencodingjson) +semantics from the Go 1.24+ `encoding/json` release for request fields. -``` -https://ik.imagekit.io/your_imagekit_id/endpoint/default-image.jpg?tr=h-300,w-400:rt-90 -``` +Required primitive fields (`int64`, `string`, etc.) feature the tag \`json:"...,required"\`. These +fields are always serialized, even their zero values. +Optional primitive types are wrapped in a `param.Opt[T]`. These fields can be set with the provided constructors, `imagekit.String(string)`, `imagekit.Int(int64)`, etc. -`UrlParam` has the following options: +Any `param.Opt[T]`, map, slice, struct or string enum uses the +tag \`json:"...,omitzero"\`. Its zero value is considered omitted. -| Option | Description | -| :----------------| :----------------------------- | -| Path | Conditional. This is the path at which the image exists. For example, `/path/to/image.jpg`. Either the `Path` or `Src` parameter needs to be specified for URL generation. | -| Src | Conditional. This is the complete URL of an image already mapped to ImageKit. For example, `https://ik.imagekit.io/your_imagekit_id/endpoint/path/to/image.jpg`. Either the `Path` or `Src` parameter needs to be specified for URL generation. | -| UrlEndpoint | Optional. The base URL to be appended before the path of the image. If not specified, the URL Endpoint specified at the time of SDK initialization is used. For example, https://ik.imagekit.io/your_imagekit_id/endpoint/ | -| Transformations | Optional. An array of objects specifying the transformation to be applied in the URL. Different steps of a [chained transformation](https://docs.imagekit.io/features/image-transformations/chained-transformations) can be specified as different objects of the array. The complete list of supported transformations in the SDK and some examples of using them are given later. -| TransformationPosition | Optional. The default value is `Path`, which places the transformation string as a path parameter in the URL. It can also be specified as `query`, which adds the transformation string as the URL's query parameter `tr`. If you use the `Src` parameter to create the URL, then the transformation string is always added as a query parameter. | -| NamedTransformation | Optional. Specifies the name of a pre-defined transformation. | -| QueryParameters | Optional. These are the other query parameters that you want to add to the final URL. These can be any query parameters and not necessarily related to ImageKit. Especially useful if you want to add some versioning parameters to your URLs. | -| Signed | Optional. Boolean. Default is `false`. If set to `true`, the SDK generates a signed image URL adding the image signature to the image URL. If you create a URL using the `Src` parameter instead of `Path`, then do correct `UrlEndpoint` for this to work. Otherwise returned URL will have the wrong signature | -| ExpireSeconds | Optional. Integer. Meant to be used along with the `Signed` parameter to specify the time in seconds from now when the URL should expire. If specified, the URL contains the expiry timestamp in the URL, and the image signature is modified accordingly. | +The `param.IsOmitted(any)` function can confirm the presence of any `omitzero` field. -#### Examples of generating URLs -**1. Chained Transformations as a query parameter** ```go +p := imagekit.ExampleParams{ + ID: "id_xxx", // required property + Name: imagekit.String("..."), // optional property -params := ikurl.UrlParam{ - Path: "default-image.jpg", - UrlEndpoint: "https://ik.imagekit.io/demo-id/", - Transformations: []map[string]any{ - { - "height": 300, - "width": 400, - }, - { - "rotation": 90, - }, - }, - TransformationPosition: ikurl.QUERY, -}, - -url, err := ik.Url(params) -``` + Point: imagekit.Point{ + X: 0, // required field will serialize as 0 + Y: imagekit.Int(1), // optional field will serialize as 1 + // ... omitted non-required fields will not be serialized + }, -**2. Sharpening and contrast transform and a progressive JPG image** - -Some transform like [Sharpening](https://docs.imagekit.io/features/image-transformations/image-enhancement-and-color-manipulation) can be added to the URL with or without any other value. To use such transforms without specifying a value, specify the value as "-" in the transformation object. Otherwise, specify the value that you want to be added to this transformation. -```go -params := ikurl.UrlParam{ - Path: "default-image.jpg", - UrlEndpoint: "https://ik.imagekit.io/demo-id/", - Transformations: []map[string]any{ - { - "effectSharpen": "-", - }, - }, + Origin: imagekit.Origin{}, // the zero value of [Origin] is considered omitted } ``` -**3. Adding overlays** +To send `null` instead of a `param.Opt[T]`, use `param.Null[T]()`. +To send `null` instead of a struct `T`, use `param.NullStruct[T]()`. -ImageKit.io enables you to apply overlays to [images](https://docs.imagekit.io/features/image-transformations/overlay-using-layers) and [videos](https://docs.imagekit.io/features/video-transformation/overlay) using the raw parameter with the concept of [layers](https://docs.imagekit.io/features/image-transformations/overlay-using-layers#layers). The raw parameter facilitates incorporating transformations directly in the URL. A layer is a distinct type of transformation that allows you to define an asset to serve as an overlay, along with its positioning and additional transformations. +```go +p.Name = param.Null[string]() // 'null' instead of string +p.Point = param.NullStruct[Point]() // 'null' instead of struct -**Text as overlays** +param.IsNull(p.Name) // true +param.IsNull(p.Point) // true +``` -You can add any text string over a base video or image using a text layer (l-text). +Request structs contain a `.SetExtraFields(map[string]any)` method which can send non-conforming +fields in the request body. Extra fields overwrite any struct fields with a matching +key. For security reasons, only use `SetExtraFields` with trusted data. -For example: +To send a custom value instead of a struct, use `param.Override[T](value)`. ```go -params := ikurl.UrlParam{ - Path: "default-image.jpg", - UrlEndpoint: "https://ik.imagekit.io/demo-id/", - Transformations: []map[string]any{ - { - "height": 300, - "width": 400, - "raw": "l-text,i-Imagekit,fs-50,l-end" - }, - }, -} -``` -**Sample Result URL** -``` -https://ik.imagekit.io/demo-id/default-image.jpg?tr=h-300,w-400,l-text,i-Imagekit,fs-50,l-end +// In cases where the API specifies a given type, +// but you want to send something else, use [SetExtraFields]: +p.SetExtraFields(map[string]any{ + "x": 0.01, // send "x" as a float instead of int +}) + +// Send a number instead of an object +custom := param.Override[imagekit.FooParams](12) ``` -**Image as overlays** +### Request unions -You can add an image over a base video or image using an image layer (l-image). +Unions are represented as a struct with fields prefixed by "Of" for each of it's variants, +only one field can be non-zero. The non-zero field will be serialized. -For example: +Sub-properties of the union can be accessed via methods on the union struct. +These methods return a mutable pointer to the underlying data, if present. ```go -params := ikurl.UrlParam{ - Path: "default-image.jpg", - UrlEndpoint: "https://ik.imagekit.io/demo-id/", - Transformations: []map[string]any{ - { - "height": 300, - "width": 400, - "raw": "l-image,i-default-image.jpg,w-100,b-10_CDDC39,l-end" - }, - }, +// Only one field can be non-zero, use param.IsOmitted() to check if a field is set +type AnimalUnionParam struct { + OfCat *Cat `json:",omitzero,inline` + OfDog *Dog `json:",omitzero,inline` } -``` -**Sample Result URL** -``` -https://ik.imagekit.io/demo-id/default-image.jpg?tr=h-300,w-400,l-image,i-default-image.jpg,w-100,b-10_CDDC39,l-end -``` - -**Solid color blocks as overlays** -You can add solid color blocks over a base video or image using an image layer (l-image). - -For example: +animal := AnimalUnionParam{ + OfCat: &Cat{ + Name: "Whiskers", + Owner: PersonParam{ + Address: AddressParam{Street: "3333 Coyote Hill Rd", Zip: 0}, + }, + }, +} -```go -params := ikurl.UrlParam{ - Path: "img/sample-video.mp4", - UrlEndpoint: "https://ik.imagekit.io/demo-id/", - Transformations: []map[string]any{ - { - "height": 300, - "width": 400, - "raw": "l-image,i-ik_canvas,bg-FF0000,w-300,h-100,l-end" - }, - }, +// Mutating a field +if address := animal.GetOwner().GetAddress(); address != nil { + address.ZipCode = 94304 } ``` -**Sample Result URL** -``` -https://ik.imagekit.io/demo-id/img/sample-video.mp4?tr=h-300,w-400,l-image,i-ik_canvas,bg-FF0000,w-300,h-100,l-end -``` -#### List of supported transformations - -See the complete list of transformations supported in ImageKit [here](https://docs.imagekit.io/features/image-transformations). The SDK gives a name to each transformation parameter e.g. `height` for `h` and `width` for `w` parameter. It makes your code more readable. If the property does not match any of the following supported options, it is added as it is. - -If you want to generate transformations in your application and add them to the URL as it is, use the raw parameter. - -| Supported Transformation Name | Translates to parameter | -|-------------------------------|-------------------------| -|height |h| -|width |w| -|aspectRatio |ar| -|quality |q| -|crop |c| -|cropMode |cm| -|x |x| -|y |y| -|xc |xc| -|yc |yc| -|focus |fo| -|format |f| -|radius |r| -|background |bg| -|border |b| -|rotation |rt| -|blur |bl| -|named |n| -|progressive |pr| -|lossless |lo| -|trim |t| -|metadata |md| -|colorProfile |cp| -|defaultImage |di| -|dpr |dpr| -|effectSharpen |e-sharpen| -|effectUSM |e-usm| -|effectContrast |e-contrast| -|effectGray |e-grayscale| -|original |orig| -|raw | `replaced by the parameter value`| - - -## File-Upload - -The SDK uploader package provides a simple interface using the `.upload()` method to upload files to the ImageKit Media Library. It accepts all the parameters supported by the [ImageKit Upload API](https://docs.imagekit.io/api-reference/upload-file-api/server-side-file-upload). - -The upload() method accept file and UploadParam. File param can be base64 encoded image, absolute HTTP URL, or io.Reader. This method returns the `UploadResponse` object and `err` if any. In addition, you can pass other parameters supported by the ImageKit upload API using the same parameter name as specified in the upload API documentation. For example, to set tags for a file at the upload time, use the tags parameter as defined in the [documentation here](https://docs.imagekit.io/api-reference/upload-file-api/server-side-file-upload). -``` -import "github.com/imagekit-developer/imagekit-go/uploader" +### Response objects -const base64Image = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" -resp, err := ik.Uploader.Upload(ctx, base64Image, uploader.UploadParam{ - FileName: "myimage.jpg" -}) +All fields in response structs are ordinary value types (not pointers or wrappers). +Response structs also include a special `JSON` field containing metadata about +each property. +```go +type Animal struct { + Name string `json:"name,nullable"` + Owners int `json:"owners"` + Age int `json:"age"` + JSON struct { + Name respjson.Field + Owner respjson.Field + Age respjson.Field + ExtraFields map[string]respjson.Field + } `json:"-"` +} ``` -## File-Management +To handle optional data, use the `.Valid()` method on the JSON field. +`.Valid()` returns true if a field is not `null`, not present, or couldn't be marshaled. -The SDK provides a simple interface for all the [media APIs mentioned here](https://docs.imagekit.io/api-reference/media-api) to manage your files. +If `.Valid()` is false, the corresponding field will simply be its zero value. -### 1. List & Search Files -List files in the media library, optionally filter and sort using `FileParams`. +```go +raw := `{"owners": 1, "name": null}` -``` -import ( - "github.com/imagekit-developer/imagekit-go" - "github.com/imagekit-developer/imagekit-go/api/media" -) +var res Animal +json.Unmarshal([]byte(raw), &res) -resp, err := ik.Media.Files(ctx, media.FilesParam{ - Skip: 10, - Limit: 500, - SearchQuery: "createdAt >= \"7d\" AND size > \"2mb\"", -}) -``` +// Accessing regular fields -### 2. Get File Details -Accepts the file ID and fetches the details as per the [API documentation here](https://docs.imagekit.io/api-reference/media-api/get-file-details). +res.Owners // 1 +res.Name // "" +res.Age // 0 -``` -resp, err := ik.Media.FileById(ctx, "file_id") -``` +// Optional field checks -### 3. Get File Version Details -Get all the details and attributes of any version of a file as per the [API documentation here](https://docs.imagekit.io/api-reference/media-api/get-file-version-details). +res.JSON.Owners.Valid() // true +res.JSON.Name.Valid() // false +res.JSON.Age.Valid() // false -``` -resp, err := ik.Media.FileVersions(ctx, media.FileVersionsParam{ - FileId: "file_id", - VersionId: "version_id", -}) +// Raw JSON values +res.JSON.Owners.Raw() // "1" +res.JSON.Name.Raw() == "null" // true +res.JSON.Name.Raw() == respjson.Null // true +res.JSON.Age.Raw() == "" // true +res.JSON.Age.Raw() == respjson.Omitted // true ``` -### 4. Get File Versions -Get all the file version details and attributes of a file as per the [API documentation here](https://docs.imagekit.io/api-reference/media-api/get-file-versions). +These `.JSON` structs also include an `ExtraFields` map containing +any properties in the json response that were not specified +in the struct. This can be useful for API features not yet +present in the SDK. -``` -resp, err := ik.Media.FileVersions(ctx, media.FileVersionsParam{ - FileId: "file_id", -}) +```go +body := res.JSON.ExtraFields["my_unexpected_field"].Raw() ``` -### 5. Update File Details -Update parameters associated with the file as per the [API documentation here](https://docs.imagekit.io/api-reference/media-api/update-file-details). +### Response Unions -``` -resp, err := ik.Media.UpdateFile(ctx, fileId, media.UpdateFileParam{ - Tags: []string{"tag_1", "tag_2"}, - RemoveAITags: []string{"car", "suv"}, -}) -``` +In responses, unions are represented by a flattened struct containing all possible fields from each of the +object variants. +To convert it to a variant use the `.AsFooVariant()` method or the `.AsAny()` method if present. -### 6. Add Tags (bulk) -Set tags to multiple files. Accepts slices of tags and file Ids. Returns slice of file ids. [API documentation here](https://docs.imagekit.io/api-reference/media-api/add-tags-bulk). +If a response value union contains primitive values, primitive fields will be alongside +the properties but prefixed with `Of` and feature the tag `json:"...,inline"`. -``` -resp, err := ik.Media.AddTags(ctx, media.TagsParam{ - FileIds: []string{"file_id_1", "file_id_2"}, - Tags: []string{"tag_1", "tag_2"}, -}) -``` - -### 7. Remove Tags (bulk) -Removes tags from multiple files. Returns slice of file IDs updated. [API documentation here](https://docs.imagekit.io/api-reference/media-api/remove-tags-bulk). +```go +type AnimalUnion struct { + // From variants [Dog], [Cat] + Owner Person `json:"owner"` + // From variant [Dog] + DogBreed string `json:"dog_breed"` + // From variant [Cat] + CatBreed string `json:"cat_breed"` + // ... + + JSON struct { + Owner respjson.Field + // ... + } `json:"-"` +} -``` -resp, err := ik.Media.RemoveTags(ctx, media.TagsParam{ - FileIds: []string{"file_id_1", "file_id_2"}, - Tags: []string{"tag_1", "tag_2"}, -}) -``` -### 8. Remove AITags (bulk) -Remove AITags in bulk API. Returns slice of file ids. [API documentation here](https://docs.imagekit.io/api-reference/media-api/remove-aitags-bulk). +// If animal variant +if animal.Owner.Address.ZipCode == "" { + panic("missing zip code") +} -``` -resp, err := ik.Media.RemoveAITags(ctx, media.AITagsParam{ - FileIds: []string{"file_id_1", "file_id_2"}, - AITags: []string{"tag_1", "tag_2"}, -}) +// Switch on the variant +switch variant := animal.AsAny().(type) { +case Dog: +case Cat: +default: + panic("unexpected type") +} ``` -### 9. Delete File -Delete a file by fileId. [API documentation here](https://docs.imagekit.io/api-reference/media-api/delete-file). -``` -resp, err := ik.Media.DeleteFile(ctx, "file_id") -``` +### RequestOptions -### 10. Delete File Version -Deletes the given version of the file. [API documentation here](https://docs.imagekit.io/api-reference/media-api/delete-file-version). -``` -resp, err := ik.Media.DeleteFileVersion(ctx, "file_id", "version_1") -``` +This library uses the functional options pattern. Functions defined in the +`option` package return a `RequestOption`, which is a closure that mutates a +`RequestConfig`. These options can be supplied to the client or at individual +requests. For example: -### 11. Delete Files (bulk) -Deletes multiple files. [API documentation here](https://docs.imagekit.io/api-reference/media-api/delete-files-bulk). +```go +client := imagekit.NewClient( + // Adds a header to every request made by the client + option.WithHeader("X-Some-Header", "custom_header_info"), +) -``` -resp, err := ik.Media.DeleteBulkFiles(ctx, media.FileIdsParam{ - FileIds: []string{"file_id1", "file_id2"}, +client.Files.Upload(context.TODO(), ..., + // Override the header + option.WithHeader("X-Some-Header", "some_other_custom_header_info"), + // Add an undocumented field to the request body, using sjson syntax + option.WithJSONSet("some.json.path", map[string]string{"my": "object"}), ) ``` -### 12. Copy File -This will copy a file from one location to another as per [API documentation here](https://docs.imagekit.io/api-reference/media-api/copy-file). +The request option `option.WithDebugLog(nil)` may be helpful while debugging. -Accepts the source file's path and destination folder path. -``` -resp, err := ik.Media.CopyFile(ctx, media.CopyFileParam{ - SourcePath: "/source/a.jpg", - DestinationPath: "/target/", - IncludeFileVersions: false, -}) -``` +See the [full list of request options](https://pkg.go.dev/github.com/imagekit-developer/imagekit-go/option). -### 13. Move File -This will move a file from one location to another as per [API documentation here](https://docs.imagekit.io/api-reference/media-api/move-file). +### Pagination -Accepts the source file's path and destination folder path. -``` -resp, err := ik.Media.MoveFile(ctx, media.MoveFileParam{ - SourcePath: "/source/a.jpg", - DestinationPath: "/target/", -}) -``` - -### 14. Rename File -Renames a file as per [API documentation here](https://docs.imagekit.io/api-reference/media-api/rename-file). -Accepts file path, new name and purge cache option. +This library provides some conveniences for working with paginated list endpoints. -``` -resp, err := ik.Media.RenameFile(ctx, media.RenameFileParam{ - FilePath: "/path/to/file.jpg", - NewFileName: "newname.jpg", - PurgeCache: true, -}) +You can use `.ListAutoPaging()` methods to iterate through items across all pages: -``` +Or you can use simple `.List()` methods to fetch a single page and receive a standard response object +with additional helper methods like `.GetNextPage()`, e.g.: -### 15. Restore File Version -Restore the file version as per [API documentation here](https://docs.imagekit.io/api-reference/media-api/restore-file-version). -Accepts string type file id and version id. -``` -resp, err := ik.Media.RestoreVersion(ctx, media.FileVersionsParam{ - FileId: "file_id", - VersionId: "version_id", -}) -``` +### Errors -### 16. Create Folder -Creates a new folder as per [API documentation here](https://docs.imagekit.io/api-reference/media-api/create-folder). `err` is not nil when the response is not 201. +When the API returns a non-success status code, we return an error with type +`*imagekit.Error`. This contains the `StatusCode`, `*http.Request`, and +`*http.Response` values of the request, as well as the JSON of the error body +(much like other response objects in the SDK). -Accepts string type folder name and parent path. +To handle errors, we recommend that you use the `errors.As` pattern: -``` -resp, err := ik.Media.CreateFolder(ctx, media.CreateFolderParam{ - FolderName: "nature", - ParentFolderPath: "/some/pics" +```go +_, err := client.Files.Upload(context.TODO(), imagekit.FileUploadParams{ + File: io.Reader(bytes.NewBuffer([]byte("https://www.example.com/public-url.jpg"))), + FileName: "file-name.jpg", +}) +if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + println(string(apierr.DumpRequest(true))) // Prints the serialized HTTP request + println(string(apierr.DumpResponse(true))) // Prints the serialized HTTP response + } + panic(err.Error()) // GET "/api/v1/files/upload": 400 Bad Request { ... } } ``` -### 17. Delete Folder -Deletes the specified folder and all nested files, their versions & folders. This action cannot be undone. Accepts string type folder name to delete. [API documentation here](https://docs.imagekit.io/api-reference/media-api/delete-folder). +When other errors occur, they are returned unwrapped; for example, +if HTTP transport fails, you might receive `*url.Error` wrapping `*net.OpError`. +### Timeouts -``` -resp, err := ik.Media.DeleteFolder(ctx, media.DeleteFolderParam{ - FolderPath: "/some/pics/nature", -}) -``` +Requests do not time out by default; use context to configure a timeout for a request lifecycle. -### 18. Copy Folder -Copies given folder to new location with or without versions info as per [API documentation here](https://docs.imagekit.io/api-reference/media-api/copy-folder). +Note that if a request is [retried](#retries), the context timeout does not start over. +To set a per-retry timeout, use `option.WithRequestTimeout()`. -``` -resp, err := ik.Media.CopyFolder(ctx, media.CopyFolderParam{ - SourceFolderPath: "source/path", - DestinationPath: "destination/", - IncludeFileVersions: false -}) +```go +// This sets the timeout for the request, including all the retries. +ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) +defer cancel() +client.Files.Upload( + ctx, + imagekit.FileUploadParams{ + File: io.Reader(bytes.NewBuffer([]byte("https://www.example.com/public-url.jpg"))), + FileName: "file-name.jpg", + }, + // This sets the per-retry timeout + option.WithRequestTimeout(20*time.Second), +) ``` -### 19. Move Folder -Moves given folder path to new location as per [API documentation here](https://docs.imagekit.io/api-reference/media-api/move-folder). +### File uploads -``` -resp, err := ik.Media.MoveFolder(ctx, media.MoveFolderParam{ - SourceFolderPath: "source/path", - DestinationPath: "destination/path", -}) -``` +Request parameters that correspond to file uploads in multipart requests are typed as +`io.Reader`. The contents of the `io.Reader` will by default be sent as a multipart form +part with the file name of "anonymous_file" and content-type of "application/octet-stream". -### 20. Bulk Job Status -Get the status of a bulk job operation by job id. Accepts string type job id. [API documentation here](https://docs.imagekit.io/api-reference/media-api/copy-move-folder-status). +The file name and content-type can be customized by implementing `Name() string` or `ContentType() +string` on the run-time type of `io.Reader`. Note that `os.File` implements `Name() string`, so a +file returned by `os.Open` will be sent with the file name on disk. -``` -resp, err := ik.BulkJobStatus(ctx, "job_id") -``` +We also provide a helper `imagekit.NewFile(reader io.Reader, filename string, contentType string)` +which can be used to wrap any `io.Reader` with the appropriate file name and content type. -### 21. Purge Cache -This will purge the CDN and ImageKit internal cache for a given URL. [API documentation here](https://docs.imagekit.io/api-reference/media-api/purge-cache). +```go +// A file from the file system +file, err := os.Open("/path/to/file") +imagekit.FileUploadParams{ + File: file, + FileName: "fileName", +} -``` -resp, err := ik.Media.PurgeCache(ctx, media.PurgeCacheParam{ - Url: "https://ik.imageki.io/your_imagekit_id/rest-of-the-file-path.jpg" -}) +// A file from a string +imagekit.FileUploadParams{ + File: strings.NewReader("my file contents"), + FileName: "fileName", +} + +// With a custom filename and contentType +imagekit.FileUploadParams{ + File: imagekit.NewFile(strings.NewReader(`{"hello": "foo"}`), "file.go", "application/json"), + FileName: "fileName", +} ``` -### 22. Purge Cache Status -Get the status of the submitted purge request. Accepts purge request id. [API documentation here](https://docs.imagekit.io/api-reference/media-api/purge-cache-status). +### Retries +Certain errors will be automatically retried 2 times by default, with a short exponential backoff. +We retry by default all connection errors, 408 Request Timeout, 409 Conflict, 429 Rate Limit, +and >=500 Internal errors. -``` -resp, err := ik.Media.PurgeCacheStatus(ctx, "request_id") -``` +You can use the `WithMaxRetries` option to configure or disable this: -## Metadata API -### 1. Get File Metadata for uploaded media files -Accepts the file ID or URL and fetches the metadata as per the [API documentation here](https://docs.imagekit.io/api-reference/metadata-api/get-image-metadata-for-uploaded-media-files). +```go +// Configure the default for all requests: +client := imagekit.NewClient( + option.WithMaxRetries(0), // default is 2 +) -``` -resp, err := ik.Metadata.FromFile(ctx, "file_id") +// Override per-request: +client.Files.Upload( + context.TODO(), + imagekit.FileUploadParams{ + File: io.Reader(bytes.NewBuffer([]byte("https://www.example.com/public-url.jpg"))), + FileName: "file-name.jpg", + }, + option.WithMaxRetries(5), +) ``` -### 2. Get File Metadata from remote url -Get image EXIF, pHash, and other metadata from ImageKit.io powered remote URL using this API as per the [API documentation here](https://docs.imagekit.io/api-reference/metadata-api/get-image-metadata-from-remote-url). +### Accessing raw response data (e.g. response headers) -``` -resp, err := ik.Metadata.FromUrl(ctx, "http://domian/a.jpg") -``` +You can access the raw HTTP response data by using the `option.WithResponseInto()` request option. This is useful when +you need to examine response headers, status codes, or other details. -## Custom Metadata fields API -Create, Update, Read and Delete custom metadata rules as per the [API documentation here](https://docs.imagekit.io/api-reference/custom-metadata-fields-api). +```go +// Create a variable to store the HTTP response +var response *http.Response +response, err := client.Files.Upload( + context.TODO(), + imagekit.FileUploadParams{ + File: io.Reader(bytes.NewBuffer([]byte("https://www.example.com/public-url.jpg"))), + FileName: "file-name.jpg", + }, + option.WithResponseInto(&response), +) +if err != nil { + // handle error +} +fmt.Printf("%+v\n", response) -### 1. Create custom metadata field -``` -import "github.com/imagekit-developer/imagekit-go/api/media/metadata" - -resp, err := ik.Metadata.CreateCustomField(ctx, metadata.CreateFieldParam{ - Name: "weight", - Label: "Weight", - Schema: metadata.Schema{ - Type: "Number", - MinValue: 1, - MaxValue: 1000, - } -}) +fmt.Printf("Status Code: %d\n", response.StatusCode) +fmt.Printf("Headers: %+#v\n", response.Header) ``` -### 2. List custom metadata fields -Accepts context and boolean flag(true|false) to get deleted fields. +### Making custom/undocumented requests -``` -resp, err := ik.Metadata.CustomFields(ctx, true) - -``` +This library is typed for convenient access to the documented API. If you need to access undocumented +endpoints, params, or response properties, the library can still be used. -### 3. Update custom metadata field +#### Undocumented endpoints -``` -resp, err := ik.Metadata.UpdateCustomField(ctx, "field_id", UpdateCustomFieldParam{ - Label: "Cost", -}) -``` +To make requests to undocumented endpoints, you can use `client.Get`, `client.Post`, and other HTTP verbs. +`RequestOptions` on the client, such as retries, will be respected when making these requests. -### 4. Delete custom metadata field -Accepts context and fieldId to delete the custom metadata field. -``` -resp, err := ik.Metadata.DeleteCustomField(ctx, "field_id") +```go +var ( + // params can be an io.Reader, a []byte, an encoding/json serializable object, + // or a "…Params" struct defined in this library. + params map[string]any + + // result can be an []byte, *http.Response, a encoding/json deserializable object, + // or a model defined in this library. + result *http.Response +) +err := client.Post(context.Background(), "/unspecified", params, &result) +if err != nil { + … +} ``` - -## Utility Functions -We have included the following commonly used utility function in this package. +#### Undocumented request params -### 1. Authentication parameter generation -This method generates a signature for a given token and timestamp using the configured private key. It is useful for client-side file upload to authenticate requests. `Token` is a random string. `Expires` is a unix timestamp by which token should expire. `Token` and `Expires` are both optional parameters. `Token` defaults to an auto-generated UUID string. `Expires` defaults to a current time + 30 minutes value. +To make requests using undocumented parameters, you may use either the `option.WithQuerySet()` +or the `option.WithJSONSet()` methods. +```go +params := FooNewParams{ + ID: "id_xxxx", + Data: FooNewParamsData{ + FirstName: imagekit.String("John"), + }, +} +client.Foo.New(context.Background(), params, option.WithJSONSet("data.last_name", "Doe")) ``` -// Using auto-generated token and expiration -resp := ik.SignToken(imagekit.SignTokenParam{}) -// Using specific token and expiration -resp := ik.SignToken(imagekit.SignTokenParam{ - Token: "token-string", - Expires: 1655379249, -}) +#### Undocumented response properties -``` +To access undocumented response properties, you may either access the raw JSON of the response as a string +with `result.JSON.RawJSON()`, or get the raw JSON of a particular field on the result with +`result.JSON.Foo.Raw()`. -## Rate Limits -Except for upload API, all ImageKit APIs are rate limited to avoid excessive request rates. +Any fields that are not present on the response struct will be saved and can be accessed by `result.JSON.ExtraFields()` which returns the extra fields as a `map[string]Field`. -Whenever backend API returns 429 status code, error of type `ErrTooManyRequests` is returned, which can be tested with `errors.Is`. The rate limit detail can be retrieved from the response metadata header. Please sleep/pause for the number of milliseconds specified by the value of `resp.ResponseMetaData.Header["X-RateLimit-Reset"]` property before making additional requests to that endpoint. +### Middleware -``` -import ( - "errors" +We provide `option.WithMiddleware` which applies the given +middleware to requests. - "github.com/imagekit-developer/imagekit-go" - "github.com/imagekit-developer/imagekit-go/metadata" - "github.com/imagekit-developer/imagekit-go/api" -) -ik, err := ImageKit.New() +```go +func Logger(req *http.Request, next option.MiddlewareNext) (res *http.Response, err error) { + // Before the request + start := time.Now() + LogReq(req) -resp, err := ik.Metadata.CustomFields(ctx, true) -if errors.Is(err, api.ErrTooManyRequests) { - log.Println("rate limit exceeded", resp.ResponseMetaData.Header["X-RateLimit-Limit"]) + // Forward the request to the next handler + res, err = next(req) + + // Handle stuff after the request + end := time.Now() + LogRes(res, err, start - end) + + return res, err } +client := imagekit.NewClient( + option.WithMiddleware(Logger), +) ``` -## Support +When multiple middlewares are provided as variadic arguments, the middlewares +are applied left to right. If `option.WithMiddleware` is given +multiple times, for example first in the client then the method, the +middleware in the client will run first and the middleware given in the method +will run next. + +You may also replace the default `http.Client` with +`option.WithHTTPClient(client)`. Only one http client is +accepted (this overwrites any previous client) and receives requests after any +middleware has been applied. + +## Semantic versioning + +This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions: + +1. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_ +2. Changes that we do not expect to impact the vast majority of users in practice. + +We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. -For any feedback or to report any issues or general implementation support, please reach out to [support@imagekit.io](mailto:support@imagekit.io) +We are keen for your feedback; please open an [issue](https://www.github.com/imagekit-developer/imagekit-go/issues) with questions, bugs, or suggestions. -## Links -* [Documentation](https://docs.imagekit.io) -* [Main website](https://imagekit.io) +## Contributing -## License -Released under the MIT license. +See [the contributing documentation](./CONTRIBUTING.md). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..8e64327 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,27 @@ +# Security Policy + +## Reporting Security Issues + +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. + +To report a security issue, please contact the Stainless team at security@stainless.com. + +## Responsible Disclosure + +We appreciate the efforts of security researchers and individuals who help us maintain the security of +SDKs we generate. If you believe you have found a security vulnerability, please adhere to responsible +disclosure practices by allowing us a reasonable amount of time to investigate and address the issue +before making any information public. + +## Reporting Non-SDK Related Security Issues + +If you encounter security issues that are not directly related to SDKs but pertain to the services +or products provided by Image Kit, please follow the respective company's security reporting guidelines. + +### Image Kit Terms and Policies + +Please contact developer@imagekit.io for any questions or concerns regarding the security of our services. + +--- + +Thank you for helping us keep the SDKs and systems they interact with secure. diff --git a/account.go b/account.go new file mode 100644 index 0000000..581f666 --- /dev/null +++ b/account.go @@ -0,0 +1,32 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit + +import ( + "github.com/imagekit-developer/imagekit-go/option" +) + +// AccountService contains methods and other services that help with interacting +// with the ImageKit API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewAccountService] method instead. +type AccountService struct { + Options []option.RequestOption + Usage AccountUsageService + Origins AccountOriginService + URLEndpoints AccountURLEndpointService +} + +// NewAccountService generates a new service that applies the given options to each +// request. These options are applied after the parent client's options (if there +// is one), and before any request-specific options. +func NewAccountService(opts ...option.RequestOption) (r AccountService) { + r = AccountService{} + r.Options = opts + r.Usage = NewAccountUsageService(opts...) + r.Origins = NewAccountOriginService(opts...) + r.URLEndpoints = NewAccountURLEndpointService(opts...) + return +} diff --git a/accountorigin.go b/accountorigin.go new file mode 100644 index 0000000..5ee82e5 --- /dev/null +++ b/accountorigin.go @@ -0,0 +1,1094 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/imagekit-developer/imagekit-go/internal/apijson" + shimjson "github.com/imagekit-developer/imagekit-go/internal/encoding/json" + "github.com/imagekit-developer/imagekit-go/internal/requestconfig" + "github.com/imagekit-developer/imagekit-go/option" + "github.com/imagekit-developer/imagekit-go/packages/param" + "github.com/imagekit-developer/imagekit-go/packages/respjson" + "github.com/imagekit-developer/imagekit-go/shared/constant" +) + +// AccountOriginService contains methods and other services that help with +// interacting with the ImageKit API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewAccountOriginService] method instead. +type AccountOriginService struct { + Options []option.RequestOption +} + +// NewAccountOriginService generates a new service that applies the given options +// to each request. These options are applied after the parent client's options (if +// there is one), and before any request-specific options. +func NewAccountOriginService(opts ...option.RequestOption) (r AccountOriginService) { + r = AccountOriginService{} + r.Options = opts + return +} + +// **Note:** This API is currently in beta. +// Creates a new origin and returns the origin object. +func (r *AccountOriginService) New(ctx context.Context, body AccountOriginNewParams, opts ...option.RequestOption) (res *OriginResponseUnion, err error) { + opts = append(r.Options[:], opts...) + path := "v1/accounts/origins" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +// **Note:** This API is currently in beta. +// Updates the origin identified by `id` and returns the updated origin object. +func (r *AccountOriginService) Update(ctx context.Context, id string, body AccountOriginUpdateParams, opts ...option.RequestOption) (res *OriginResponseUnion, err error) { + opts = append(r.Options[:], opts...) + if id == "" { + err = errors.New("missing required id parameter") + return + } + path := fmt.Sprintf("v1/accounts/origins/%s", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPut, path, body, &res, opts...) + return +} + +// **Note:** This API is currently in beta. +// Returns an array of all configured origins for the current account. +func (r *AccountOriginService) List(ctx context.Context, opts ...option.RequestOption) (res *[]OriginResponseUnion, err error) { + opts = append(r.Options[:], opts...) + path := "v1/accounts/origins" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return +} + +// **Note:** This API is currently in beta. +// Permanently removes the origin identified by `id`. If the origin is in use by +// any URL‑endpoints, the API will return an error. +func (r *AccountOriginService) Delete(ctx context.Context, id string, opts ...option.RequestOption) (err error) { + opts = append(r.Options[:], opts...) + opts = append([]option.RequestOption{option.WithHeader("Accept", "")}, opts...) + if id == "" { + err = errors.New("missing required id parameter") + return + } + path := fmt.Sprintf("v1/accounts/origins/%s", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, nil, opts...) + return +} + +// **Note:** This API is currently in beta. +// Retrieves the origin identified by `id`. +func (r *AccountOriginService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *OriginResponseUnion, err error) { + opts = append(r.Options[:], opts...) + if id == "" { + err = errors.New("missing required id parameter") + return + } + path := fmt.Sprintf("v1/accounts/origins/%s", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return +} + +func OriginRequestParamOfWebFolder(baseURL string, name string) OriginRequestUnionParam { + var webFolder OriginRequestWebFolderParam + webFolder.BaseURL = baseURL + webFolder.Name = name + return OriginRequestUnionParam{OfWebFolder: &webFolder} +} + +func OriginRequestParamOfWebProxy(name string) OriginRequestUnionParam { + var webProxy OriginRequestWebProxyParam + webProxy.Name = name + return OriginRequestUnionParam{OfWebProxy: &webProxy} +} + +// Only one field can be non-zero. +// +// Use [param.IsOmitted] to confirm if a field is set. +type OriginRequestUnionParam struct { + OfS3 *OriginRequestS3Param `json:",omitzero,inline"` + OfS3Compatible *OriginRequestS3CompatibleParam `json:",omitzero,inline"` + OfCloudinaryBackup *OriginRequestCloudinaryBackupParam `json:",omitzero,inline"` + OfWebFolder *OriginRequestWebFolderParam `json:",omitzero,inline"` + OfWebProxy *OriginRequestWebProxyParam `json:",omitzero,inline"` + OfGcs *OriginRequestGcsParam `json:",omitzero,inline"` + OfAzureBlob *OriginRequestAzureBlobParam `json:",omitzero,inline"` + OfAkeneoPim *OriginRequestAkeneoPimParam `json:",omitzero,inline"` + paramUnion +} + +func (u OriginRequestUnionParam) MarshalJSON() ([]byte, error) { + return param.MarshalUnion(u, u.OfS3, + u.OfS3Compatible, + u.OfCloudinaryBackup, + u.OfWebFolder, + u.OfWebProxy, + u.OfGcs, + u.OfAzureBlob, + u.OfAkeneoPim) +} +func (u *OriginRequestUnionParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, u) +} + +func (u *OriginRequestUnionParam) asAny() any { + if !param.IsOmitted(u.OfS3) { + return u.OfS3 + } else if !param.IsOmitted(u.OfS3Compatible) { + return u.OfS3Compatible + } else if !param.IsOmitted(u.OfCloudinaryBackup) { + return u.OfCloudinaryBackup + } else if !param.IsOmitted(u.OfWebFolder) { + return u.OfWebFolder + } else if !param.IsOmitted(u.OfWebProxy) { + return u.OfWebProxy + } else if !param.IsOmitted(u.OfGcs) { + return u.OfGcs + } else if !param.IsOmitted(u.OfAzureBlob) { + return u.OfAzureBlob + } else if !param.IsOmitted(u.OfAkeneoPim) { + return u.OfAkeneoPim + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u OriginRequestUnionParam) GetEndpoint() *string { + if vt := u.OfS3Compatible; vt != nil { + return &vt.Endpoint + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u OriginRequestUnionParam) GetS3ForcePathStyle() *bool { + if vt := u.OfS3Compatible; vt != nil && vt.S3ForcePathStyle.Valid() { + return &vt.S3ForcePathStyle.Value + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u OriginRequestUnionParam) GetForwardHostHeaderToOrigin() *bool { + if vt := u.OfWebFolder; vt != nil && vt.ForwardHostHeaderToOrigin.Valid() { + return &vt.ForwardHostHeaderToOrigin.Value + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u OriginRequestUnionParam) GetClientEmail() *string { + if vt := u.OfGcs; vt != nil { + return &vt.ClientEmail + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u OriginRequestUnionParam) GetPrivateKey() *string { + if vt := u.OfGcs; vt != nil { + return &vt.PrivateKey + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u OriginRequestUnionParam) GetAccountName() *string { + if vt := u.OfAzureBlob; vt != nil { + return &vt.AccountName + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u OriginRequestUnionParam) GetContainer() *string { + if vt := u.OfAzureBlob; vt != nil { + return &vt.Container + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u OriginRequestUnionParam) GetSasToken() *string { + if vt := u.OfAzureBlob; vt != nil { + return &vt.SasToken + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u OriginRequestUnionParam) GetClientID() *string { + if vt := u.OfAkeneoPim; vt != nil { + return &vt.ClientID + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u OriginRequestUnionParam) GetClientSecret() *string { + if vt := u.OfAkeneoPim; vt != nil { + return &vt.ClientSecret + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u OriginRequestUnionParam) GetPassword() *string { + if vt := u.OfAkeneoPim; vt != nil { + return &vt.Password + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u OriginRequestUnionParam) GetUsername() *string { + if vt := u.OfAkeneoPim; vt != nil { + return &vt.Username + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u OriginRequestUnionParam) GetAccessKey() *string { + if vt := u.OfS3; vt != nil { + return (*string)(&vt.AccessKey) + } else if vt := u.OfS3Compatible; vt != nil { + return (*string)(&vt.AccessKey) + } else if vt := u.OfCloudinaryBackup; vt != nil { + return (*string)(&vt.AccessKey) + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u OriginRequestUnionParam) GetBucket() *string { + if vt := u.OfS3; vt != nil { + return (*string)(&vt.Bucket) + } else if vt := u.OfS3Compatible; vt != nil { + return (*string)(&vt.Bucket) + } else if vt := u.OfCloudinaryBackup; vt != nil { + return (*string)(&vt.Bucket) + } else if vt := u.OfGcs; vt != nil { + return (*string)(&vt.Bucket) + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u OriginRequestUnionParam) GetName() *string { + if vt := u.OfS3; vt != nil { + return (*string)(&vt.Name) + } else if vt := u.OfS3Compatible; vt != nil { + return (*string)(&vt.Name) + } else if vt := u.OfCloudinaryBackup; vt != nil { + return (*string)(&vt.Name) + } else if vt := u.OfWebFolder; vt != nil { + return (*string)(&vt.Name) + } else if vt := u.OfWebProxy; vt != nil { + return (*string)(&vt.Name) + } else if vt := u.OfGcs; vt != nil { + return (*string)(&vt.Name) + } else if vt := u.OfAzureBlob; vt != nil { + return (*string)(&vt.Name) + } else if vt := u.OfAkeneoPim; vt != nil { + return (*string)(&vt.Name) + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u OriginRequestUnionParam) GetSecretKey() *string { + if vt := u.OfS3; vt != nil { + return (*string)(&vt.SecretKey) + } else if vt := u.OfS3Compatible; vt != nil { + return (*string)(&vt.SecretKey) + } else if vt := u.OfCloudinaryBackup; vt != nil { + return (*string)(&vt.SecretKey) + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u OriginRequestUnionParam) GetType() *string { + if vt := u.OfS3; vt != nil { + return (*string)(&vt.Type) + } else if vt := u.OfS3Compatible; vt != nil { + return (*string)(&vt.Type) + } else if vt := u.OfCloudinaryBackup; vt != nil { + return (*string)(&vt.Type) + } else if vt := u.OfWebFolder; vt != nil { + return (*string)(&vt.Type) + } else if vt := u.OfWebProxy; vt != nil { + return (*string)(&vt.Type) + } else if vt := u.OfGcs; vt != nil { + return (*string)(&vt.Type) + } else if vt := u.OfAzureBlob; vt != nil { + return (*string)(&vt.Type) + } else if vt := u.OfAkeneoPim; vt != nil { + return (*string)(&vt.Type) + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u OriginRequestUnionParam) GetBaseURLForCanonicalHeader() *string { + if vt := u.OfS3; vt != nil && vt.BaseURLForCanonicalHeader.Valid() { + return &vt.BaseURLForCanonicalHeader.Value + } else if vt := u.OfS3Compatible; vt != nil && vt.BaseURLForCanonicalHeader.Valid() { + return &vt.BaseURLForCanonicalHeader.Value + } else if vt := u.OfCloudinaryBackup; vt != nil && vt.BaseURLForCanonicalHeader.Valid() { + return &vt.BaseURLForCanonicalHeader.Value + } else if vt := u.OfWebFolder; vt != nil && vt.BaseURLForCanonicalHeader.Valid() { + return &vt.BaseURLForCanonicalHeader.Value + } else if vt := u.OfWebProxy; vt != nil && vt.BaseURLForCanonicalHeader.Valid() { + return &vt.BaseURLForCanonicalHeader.Value + } else if vt := u.OfGcs; vt != nil && vt.BaseURLForCanonicalHeader.Valid() { + return &vt.BaseURLForCanonicalHeader.Value + } else if vt := u.OfAzureBlob; vt != nil && vt.BaseURLForCanonicalHeader.Valid() { + return &vt.BaseURLForCanonicalHeader.Value + } else if vt := u.OfAkeneoPim; vt != nil && vt.BaseURLForCanonicalHeader.Valid() { + return &vt.BaseURLForCanonicalHeader.Value + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u OriginRequestUnionParam) GetIncludeCanonicalHeader() *bool { + if vt := u.OfS3; vt != nil && vt.IncludeCanonicalHeader.Valid() { + return &vt.IncludeCanonicalHeader.Value + } else if vt := u.OfS3Compatible; vt != nil && vt.IncludeCanonicalHeader.Valid() { + return &vt.IncludeCanonicalHeader.Value + } else if vt := u.OfCloudinaryBackup; vt != nil && vt.IncludeCanonicalHeader.Valid() { + return &vt.IncludeCanonicalHeader.Value + } else if vt := u.OfWebFolder; vt != nil && vt.IncludeCanonicalHeader.Valid() { + return &vt.IncludeCanonicalHeader.Value + } else if vt := u.OfWebProxy; vt != nil && vt.IncludeCanonicalHeader.Valid() { + return &vt.IncludeCanonicalHeader.Value + } else if vt := u.OfGcs; vt != nil && vt.IncludeCanonicalHeader.Valid() { + return &vt.IncludeCanonicalHeader.Value + } else if vt := u.OfAzureBlob; vt != nil && vt.IncludeCanonicalHeader.Valid() { + return &vt.IncludeCanonicalHeader.Value + } else if vt := u.OfAkeneoPim; vt != nil && vt.IncludeCanonicalHeader.Valid() { + return &vt.IncludeCanonicalHeader.Value + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u OriginRequestUnionParam) GetPrefix() *string { + if vt := u.OfS3; vt != nil && vt.Prefix.Valid() { + return &vt.Prefix.Value + } else if vt := u.OfS3Compatible; vt != nil && vt.Prefix.Valid() { + return &vt.Prefix.Value + } else if vt := u.OfCloudinaryBackup; vt != nil && vt.Prefix.Valid() { + return &vt.Prefix.Value + } else if vt := u.OfGcs; vt != nil && vt.Prefix.Valid() { + return &vt.Prefix.Value + } else if vt := u.OfAzureBlob; vt != nil && vt.Prefix.Valid() { + return &vt.Prefix.Value + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u OriginRequestUnionParam) GetBaseURL() *string { + if vt := u.OfWebFolder; vt != nil { + return (*string)(&vt.BaseURL) + } else if vt := u.OfAkeneoPim; vt != nil { + return (*string)(&vt.BaseURL) + } + return nil +} + +func init() { + apijson.RegisterUnion[OriginRequestUnionParam]( + "type", + apijson.Discriminator[OriginRequestS3Param]("S3"), + apijson.Discriminator[OriginRequestS3CompatibleParam]("S3_COMPATIBLE"), + apijson.Discriminator[OriginRequestCloudinaryBackupParam]("CLOUDINARY_BACKUP"), + apijson.Discriminator[OriginRequestWebFolderParam]("WEB_FOLDER"), + apijson.Discriminator[OriginRequestWebProxyParam]("WEB_PROXY"), + apijson.Discriminator[OriginRequestGcsParam]("GCS"), + apijson.Discriminator[OriginRequestAzureBlobParam]("AZURE_BLOB"), + apijson.Discriminator[OriginRequestAkeneoPimParam]("AKENEO_PIM"), + ) +} + +// The properties AccessKey, Bucket, Name, SecretKey, Type are required. +type OriginRequestS3Param struct { + // Access key for the bucket. + AccessKey string `json:"accessKey,required"` + // S3 bucket name. + Bucket string `json:"bucket,required"` + // Display name of the origin. + Name string `json:"name,required"` + // Secret key for the bucket. + SecretKey string `json:"secretKey,required"` + // URL used in the Canonical header (if enabled). + BaseURLForCanonicalHeader param.Opt[string] `json:"baseUrlForCanonicalHeader,omitzero" format:"uri"` + // Whether to send a Canonical header. + IncludeCanonicalHeader param.Opt[bool] `json:"includeCanonicalHeader,omitzero"` + // Path prefix inside the bucket. + Prefix param.Opt[string] `json:"prefix,omitzero"` + // This field can be elided, and will marshal its zero value as "S3". + Type constant.S3 `json:"type,required"` + paramObj +} + +func (r OriginRequestS3Param) MarshalJSON() (data []byte, err error) { + type shadow OriginRequestS3Param + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *OriginRequestS3Param) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// The properties AccessKey, Bucket, Endpoint, Name, SecretKey, Type are required. +type OriginRequestS3CompatibleParam struct { + // Access key for the bucket. + AccessKey string `json:"accessKey,required"` + // S3 bucket name. + Bucket string `json:"bucket,required"` + // Custom S3-compatible endpoint. + Endpoint string `json:"endpoint,required" format:"uri"` + // Display name of the origin. + Name string `json:"name,required"` + // Secret key for the bucket. + SecretKey string `json:"secretKey,required"` + // URL used in the Canonical header (if enabled). + BaseURLForCanonicalHeader param.Opt[string] `json:"baseUrlForCanonicalHeader,omitzero" format:"uri"` + // Whether to send a Canonical header. + IncludeCanonicalHeader param.Opt[bool] `json:"includeCanonicalHeader,omitzero"` + // Path prefix inside the bucket. + Prefix param.Opt[string] `json:"prefix,omitzero"` + // Use path-style S3 URLs? + S3ForcePathStyle param.Opt[bool] `json:"s3ForcePathStyle,omitzero"` + // This field can be elided, and will marshal its zero value as "S3_COMPATIBLE". + Type constant.S3Compatible `json:"type,required"` + paramObj +} + +func (r OriginRequestS3CompatibleParam) MarshalJSON() (data []byte, err error) { + type shadow OriginRequestS3CompatibleParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *OriginRequestS3CompatibleParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// The properties AccessKey, Bucket, Name, SecretKey, Type are required. +type OriginRequestCloudinaryBackupParam struct { + // Access key for the bucket. + AccessKey string `json:"accessKey,required"` + // S3 bucket name. + Bucket string `json:"bucket,required"` + // Display name of the origin. + Name string `json:"name,required"` + // Secret key for the bucket. + SecretKey string `json:"secretKey,required"` + // URL used in the Canonical header (if enabled). + BaseURLForCanonicalHeader param.Opt[string] `json:"baseUrlForCanonicalHeader,omitzero" format:"uri"` + // Whether to send a Canonical header. + IncludeCanonicalHeader param.Opt[bool] `json:"includeCanonicalHeader,omitzero"` + // Path prefix inside the bucket. + Prefix param.Opt[string] `json:"prefix,omitzero"` + // This field can be elided, and will marshal its zero value as + // "CLOUDINARY_BACKUP". + Type constant.CloudinaryBackup `json:"type,required"` + paramObj +} + +func (r OriginRequestCloudinaryBackupParam) MarshalJSON() (data []byte, err error) { + type shadow OriginRequestCloudinaryBackupParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *OriginRequestCloudinaryBackupParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// The properties BaseURL, Name, Type are required. +type OriginRequestWebFolderParam struct { + // Root URL for the web folder origin. + BaseURL string `json:"baseUrl,required" format:"uri"` + // Display name of the origin. + Name string `json:"name,required"` + // URL used in the Canonical header (if enabled). + BaseURLForCanonicalHeader param.Opt[string] `json:"baseUrlForCanonicalHeader,omitzero" format:"uri"` + // Forward the Host header to origin? + ForwardHostHeaderToOrigin param.Opt[bool] `json:"forwardHostHeaderToOrigin,omitzero"` + // Whether to send a Canonical header. + IncludeCanonicalHeader param.Opt[bool] `json:"includeCanonicalHeader,omitzero"` + // This field can be elided, and will marshal its zero value as "WEB_FOLDER". + Type constant.WebFolder `json:"type,required"` + paramObj +} + +func (r OriginRequestWebFolderParam) MarshalJSON() (data []byte, err error) { + type shadow OriginRequestWebFolderParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *OriginRequestWebFolderParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// The properties Name, Type are required. +type OriginRequestWebProxyParam struct { + // Display name of the origin. + Name string `json:"name,required"` + // URL used in the Canonical header (if enabled). + BaseURLForCanonicalHeader param.Opt[string] `json:"baseUrlForCanonicalHeader,omitzero" format:"uri"` + // Whether to send a Canonical header. + IncludeCanonicalHeader param.Opt[bool] `json:"includeCanonicalHeader,omitzero"` + // This field can be elided, and will marshal its zero value as "WEB_PROXY". + Type constant.WebProxy `json:"type,required"` + paramObj +} + +func (r OriginRequestWebProxyParam) MarshalJSON() (data []byte, err error) { + type shadow OriginRequestWebProxyParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *OriginRequestWebProxyParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// The properties Bucket, ClientEmail, Name, PrivateKey, Type are required. +type OriginRequestGcsParam struct { + Bucket string `json:"bucket,required"` + ClientEmail string `json:"clientEmail,required" format:"email"` + // Display name of the origin. + Name string `json:"name,required"` + PrivateKey string `json:"privateKey,required"` + // URL used in the Canonical header (if enabled). + BaseURLForCanonicalHeader param.Opt[string] `json:"baseUrlForCanonicalHeader,omitzero" format:"uri"` + // Whether to send a Canonical header. + IncludeCanonicalHeader param.Opt[bool] `json:"includeCanonicalHeader,omitzero"` + Prefix param.Opt[string] `json:"prefix,omitzero"` + // This field can be elided, and will marshal its zero value as "GCS". + Type constant.Gcs `json:"type,required"` + paramObj +} + +func (r OriginRequestGcsParam) MarshalJSON() (data []byte, err error) { + type shadow OriginRequestGcsParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *OriginRequestGcsParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// The properties AccountName, Container, Name, SasToken, Type are required. +type OriginRequestAzureBlobParam struct { + AccountName string `json:"accountName,required"` + Container string `json:"container,required"` + // Display name of the origin. + Name string `json:"name,required"` + SasToken string `json:"sasToken,required"` + // URL used in the Canonical header (if enabled). + BaseURLForCanonicalHeader param.Opt[string] `json:"baseUrlForCanonicalHeader,omitzero" format:"uri"` + // Whether to send a Canonical header. + IncludeCanonicalHeader param.Opt[bool] `json:"includeCanonicalHeader,omitzero"` + Prefix param.Opt[string] `json:"prefix,omitzero"` + // This field can be elided, and will marshal its zero value as "AZURE_BLOB". + Type constant.AzureBlob `json:"type,required"` + paramObj +} + +func (r OriginRequestAzureBlobParam) MarshalJSON() (data []byte, err error) { + type shadow OriginRequestAzureBlobParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *OriginRequestAzureBlobParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// The properties BaseURL, ClientID, ClientSecret, Name, Password, Type, Username +// are required. +type OriginRequestAkeneoPimParam struct { + // Akeneo instance base URL. + BaseURL string `json:"baseUrl,required" format:"uri"` + // Akeneo API client ID. + ClientID string `json:"clientId,required"` + // Akeneo API client secret. + ClientSecret string `json:"clientSecret,required"` + // Display name of the origin. + Name string `json:"name,required"` + // Akeneo API password. + Password string `json:"password,required"` + // Akeneo API username. + Username string `json:"username,required"` + // URL used in the Canonical header (if enabled). + BaseURLForCanonicalHeader param.Opt[string] `json:"baseUrlForCanonicalHeader,omitzero" format:"uri"` + // Whether to send a Canonical header. + IncludeCanonicalHeader param.Opt[bool] `json:"includeCanonicalHeader,omitzero"` + // This field can be elided, and will marshal its zero value as "AKENEO_PIM". + Type constant.AkeneoPim `json:"type,required"` + paramObj +} + +func (r OriginRequestAkeneoPimParam) MarshalJSON() (data []byte, err error) { + type shadow OriginRequestAkeneoPimParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *OriginRequestAkeneoPimParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// OriginResponseUnion contains all possible properties and values from +// [OriginResponseS3], [OriginResponseS3Compatible], +// [OriginResponseCloudinaryBackup], [OriginResponseWebFolder], +// [OriginResponseWebProxy], [OriginResponseGcs], [OriginResponseAzureBlob], +// [OriginResponseAkeneoPim]. +// +// Use the [OriginResponseUnion.AsAny] method to switch on the variant. +// +// Use the methods beginning with 'As' to cast the union to one of its variants. +type OriginResponseUnion struct { + ID string `json:"id"` + Bucket string `json:"bucket"` + IncludeCanonicalHeader bool `json:"includeCanonicalHeader"` + Name string `json:"name"` + Prefix string `json:"prefix"` + // Any of "S3", "S3_COMPATIBLE", "CLOUDINARY_BACKUP", "WEB_FOLDER", "WEB_PROXY", + // "GCS", "AZURE_BLOB", "AKENEO_PIM". + Type string `json:"type"` + BaseURLForCanonicalHeader string `json:"baseUrlForCanonicalHeader"` + // This field is from variant [OriginResponseS3Compatible]. + Endpoint string `json:"endpoint"` + // This field is from variant [OriginResponseS3Compatible]. + S3ForcePathStyle bool `json:"s3ForcePathStyle"` + BaseURL string `json:"baseUrl"` + // This field is from variant [OriginResponseWebFolder]. + ForwardHostHeaderToOrigin bool `json:"forwardHostHeaderToOrigin"` + // This field is from variant [OriginResponseGcs]. + ClientEmail string `json:"clientEmail"` + // This field is from variant [OriginResponseAzureBlob]. + AccountName string `json:"accountName"` + // This field is from variant [OriginResponseAzureBlob]. + Container string `json:"container"` + JSON struct { + ID respjson.Field + Bucket respjson.Field + IncludeCanonicalHeader respjson.Field + Name respjson.Field + Prefix respjson.Field + Type respjson.Field + BaseURLForCanonicalHeader respjson.Field + Endpoint respjson.Field + S3ForcePathStyle respjson.Field + BaseURL respjson.Field + ForwardHostHeaderToOrigin respjson.Field + ClientEmail respjson.Field + AccountName respjson.Field + Container respjson.Field + raw string + } `json:"-"` +} + +// anyOriginResponse is implemented by each variant of [OriginResponseUnion] to add +// type safety for the return type of [OriginResponseUnion.AsAny] +type anyOriginResponse interface { + implOriginResponseUnion() +} + +func (OriginResponseS3) implOriginResponseUnion() {} +func (OriginResponseS3Compatible) implOriginResponseUnion() {} +func (OriginResponseCloudinaryBackup) implOriginResponseUnion() {} +func (OriginResponseWebFolder) implOriginResponseUnion() {} +func (OriginResponseWebProxy) implOriginResponseUnion() {} +func (OriginResponseGcs) implOriginResponseUnion() {} +func (OriginResponseAzureBlob) implOriginResponseUnion() {} +func (OriginResponseAkeneoPim) implOriginResponseUnion() {} + +// Use the following switch statement to find the correct variant +// +// switch variant := OriginResponseUnion.AsAny().(type) { +// case imagekit.OriginResponseS3: +// case imagekit.OriginResponseS3Compatible: +// case imagekit.OriginResponseCloudinaryBackup: +// case imagekit.OriginResponseWebFolder: +// case imagekit.OriginResponseWebProxy: +// case imagekit.OriginResponseGcs: +// case imagekit.OriginResponseAzureBlob: +// case imagekit.OriginResponseAkeneoPim: +// default: +// fmt.Errorf("no variant present") +// } +func (u OriginResponseUnion) AsAny() anyOriginResponse { + switch u.Type { + case "S3": + return u.AsS3() + case "S3_COMPATIBLE": + return u.AsS3Compatible() + case "CLOUDINARY_BACKUP": + return u.AsCloudinaryBackup() + case "WEB_FOLDER": + return u.AsWebFolder() + case "WEB_PROXY": + return u.AsWebProxy() + case "GCS": + return u.AsGcs() + case "AZURE_BLOB": + return u.AsAzureBlob() + case "AKENEO_PIM": + return u.AsAkeneoPim() + } + return nil +} + +func (u OriginResponseUnion) AsS3() (v OriginResponseS3) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u OriginResponseUnion) AsS3Compatible() (v OriginResponseS3Compatible) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u OriginResponseUnion) AsCloudinaryBackup() (v OriginResponseCloudinaryBackup) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u OriginResponseUnion) AsWebFolder() (v OriginResponseWebFolder) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u OriginResponseUnion) AsWebProxy() (v OriginResponseWebProxy) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u OriginResponseUnion) AsGcs() (v OriginResponseGcs) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u OriginResponseUnion) AsAzureBlob() (v OriginResponseAzureBlob) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u OriginResponseUnion) AsAkeneoPim() (v OriginResponseAkeneoPim) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +// Returns the unmodified JSON received from the API +func (u OriginResponseUnion) RawJSON() string { return u.JSON.raw } + +func (r *OriginResponseUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type OriginResponseS3 struct { + // Unique identifier for the origin. This is generated by ImageKit when you create + // a new origin. + ID string `json:"id,required"` + // S3 bucket name. + Bucket string `json:"bucket,required"` + // Whether to send a Canonical header. + IncludeCanonicalHeader bool `json:"includeCanonicalHeader,required"` + // Display name of the origin. + Name string `json:"name,required"` + // Path prefix inside the bucket. + Prefix string `json:"prefix,required"` + Type constant.S3 `json:"type,required"` + // URL used in the Canonical header (if enabled). + BaseURLForCanonicalHeader string `json:"baseUrlForCanonicalHeader" format:"uri"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ID respjson.Field + Bucket respjson.Field + IncludeCanonicalHeader respjson.Field + Name respjson.Field + Prefix respjson.Field + Type respjson.Field + BaseURLForCanonicalHeader respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r OriginResponseS3) RawJSON() string { return r.JSON.raw } +func (r *OriginResponseS3) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type OriginResponseS3Compatible struct { + // Unique identifier for the origin. This is generated by ImageKit when you create + // a new origin. + ID string `json:"id,required"` + // S3 bucket name. + Bucket string `json:"bucket,required"` + // Custom S3-compatible endpoint. + Endpoint string `json:"endpoint,required" format:"uri"` + // Whether to send a Canonical header. + IncludeCanonicalHeader bool `json:"includeCanonicalHeader,required"` + // Display name of the origin. + Name string `json:"name,required"` + // Path prefix inside the bucket. + Prefix string `json:"prefix,required"` + // Use path-style S3 URLs? + S3ForcePathStyle bool `json:"s3ForcePathStyle,required"` + Type constant.S3Compatible `json:"type,required"` + // URL used in the Canonical header (if enabled). + BaseURLForCanonicalHeader string `json:"baseUrlForCanonicalHeader" format:"uri"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ID respjson.Field + Bucket respjson.Field + Endpoint respjson.Field + IncludeCanonicalHeader respjson.Field + Name respjson.Field + Prefix respjson.Field + S3ForcePathStyle respjson.Field + Type respjson.Field + BaseURLForCanonicalHeader respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r OriginResponseS3Compatible) RawJSON() string { return r.JSON.raw } +func (r *OriginResponseS3Compatible) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type OriginResponseCloudinaryBackup struct { + // Unique identifier for the origin. This is generated by ImageKit when you create + // a new origin. + ID string `json:"id,required"` + // S3 bucket name. + Bucket string `json:"bucket,required"` + // Whether to send a Canonical header. + IncludeCanonicalHeader bool `json:"includeCanonicalHeader,required"` + // Display name of the origin. + Name string `json:"name,required"` + // Path prefix inside the bucket. + Prefix string `json:"prefix,required"` + Type constant.CloudinaryBackup `json:"type,required"` + // URL used in the Canonical header (if enabled). + BaseURLForCanonicalHeader string `json:"baseUrlForCanonicalHeader" format:"uri"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ID respjson.Field + Bucket respjson.Field + IncludeCanonicalHeader respjson.Field + Name respjson.Field + Prefix respjson.Field + Type respjson.Field + BaseURLForCanonicalHeader respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r OriginResponseCloudinaryBackup) RawJSON() string { return r.JSON.raw } +func (r *OriginResponseCloudinaryBackup) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type OriginResponseWebFolder struct { + // Unique identifier for the origin. This is generated by ImageKit when you create + // a new origin. + ID string `json:"id,required"` + // Root URL for the web folder origin. + BaseURL string `json:"baseUrl,required" format:"uri"` + // Forward the Host header to origin? + ForwardHostHeaderToOrigin bool `json:"forwardHostHeaderToOrigin,required"` + // Whether to send a Canonical header. + IncludeCanonicalHeader bool `json:"includeCanonicalHeader,required"` + // Display name of the origin. + Name string `json:"name,required"` + Type constant.WebFolder `json:"type,required"` + // URL used in the Canonical header (if enabled). + BaseURLForCanonicalHeader string `json:"baseUrlForCanonicalHeader" format:"uri"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ID respjson.Field + BaseURL respjson.Field + ForwardHostHeaderToOrigin respjson.Field + IncludeCanonicalHeader respjson.Field + Name respjson.Field + Type respjson.Field + BaseURLForCanonicalHeader respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r OriginResponseWebFolder) RawJSON() string { return r.JSON.raw } +func (r *OriginResponseWebFolder) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type OriginResponseWebProxy struct { + // Unique identifier for the origin. This is generated by ImageKit when you create + // a new origin. + ID string `json:"id,required"` + // Whether to send a Canonical header. + IncludeCanonicalHeader bool `json:"includeCanonicalHeader,required"` + // Display name of the origin. + Name string `json:"name,required"` + Type constant.WebProxy `json:"type,required"` + // URL used in the Canonical header (if enabled). + BaseURLForCanonicalHeader string `json:"baseUrlForCanonicalHeader" format:"uri"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ID respjson.Field + IncludeCanonicalHeader respjson.Field + Name respjson.Field + Type respjson.Field + BaseURLForCanonicalHeader respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r OriginResponseWebProxy) RawJSON() string { return r.JSON.raw } +func (r *OriginResponseWebProxy) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type OriginResponseGcs struct { + // Unique identifier for the origin. This is generated by ImageKit when you create + // a new origin. + ID string `json:"id,required"` + Bucket string `json:"bucket,required"` + ClientEmail string `json:"clientEmail,required" format:"email"` + // Whether to send a Canonical header. + IncludeCanonicalHeader bool `json:"includeCanonicalHeader,required"` + // Display name of the origin. + Name string `json:"name,required"` + Prefix string `json:"prefix,required"` + Type constant.Gcs `json:"type,required"` + // URL used in the Canonical header (if enabled). + BaseURLForCanonicalHeader string `json:"baseUrlForCanonicalHeader" format:"uri"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ID respjson.Field + Bucket respjson.Field + ClientEmail respjson.Field + IncludeCanonicalHeader respjson.Field + Name respjson.Field + Prefix respjson.Field + Type respjson.Field + BaseURLForCanonicalHeader respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r OriginResponseGcs) RawJSON() string { return r.JSON.raw } +func (r *OriginResponseGcs) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type OriginResponseAzureBlob struct { + // Unique identifier for the origin. This is generated by ImageKit when you create + // a new origin. + ID string `json:"id,required"` + AccountName string `json:"accountName,required"` + Container string `json:"container,required"` + // Whether to send a Canonical header. + IncludeCanonicalHeader bool `json:"includeCanonicalHeader,required"` + // Display name of the origin. + Name string `json:"name,required"` + Prefix string `json:"prefix,required"` + Type constant.AzureBlob `json:"type,required"` + // URL used in the Canonical header (if enabled). + BaseURLForCanonicalHeader string `json:"baseUrlForCanonicalHeader" format:"uri"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ID respjson.Field + AccountName respjson.Field + Container respjson.Field + IncludeCanonicalHeader respjson.Field + Name respjson.Field + Prefix respjson.Field + Type respjson.Field + BaseURLForCanonicalHeader respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r OriginResponseAzureBlob) RawJSON() string { return r.JSON.raw } +func (r *OriginResponseAzureBlob) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type OriginResponseAkeneoPim struct { + // Unique identifier for the origin. This is generated by ImageKit when you create + // a new origin. + ID string `json:"id,required"` + // Akeneo instance base URL. + BaseURL string `json:"baseUrl,required" format:"uri"` + // Whether to send a Canonical header. + IncludeCanonicalHeader bool `json:"includeCanonicalHeader,required"` + // Display name of the origin. + Name string `json:"name,required"` + Type constant.AkeneoPim `json:"type,required"` + // URL used in the Canonical header (if enabled). + BaseURLForCanonicalHeader string `json:"baseUrlForCanonicalHeader" format:"uri"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ID respjson.Field + BaseURL respjson.Field + IncludeCanonicalHeader respjson.Field + Name respjson.Field + Type respjson.Field + BaseURLForCanonicalHeader respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r OriginResponseAkeneoPim) RawJSON() string { return r.JSON.raw } +func (r *OriginResponseAkeneoPim) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type AccountOriginNewParams struct { + // Schema for origin request resources. + OriginRequest OriginRequestUnionParam + paramObj +} + +func (r AccountOriginNewParams) MarshalJSON() (data []byte, err error) { + return shimjson.Marshal(r.OriginRequest) +} +func (r *AccountOriginNewParams) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &r.OriginRequest) +} + +type AccountOriginUpdateParams struct { + // Schema for origin request resources. + OriginRequest OriginRequestUnionParam + paramObj +} + +func (r AccountOriginUpdateParams) MarshalJSON() (data []byte, err error) { + return shimjson.Marshal(r.OriginRequest) +} +func (r *AccountOriginUpdateParams) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &r.OriginRequest) +} diff --git a/accountorigin_test.go b/accountorigin_test.go new file mode 100644 index 0000000..2befd99 --- /dev/null +++ b/accountorigin_test.go @@ -0,0 +1,162 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/imagekit-developer/imagekit-go" + "github.com/imagekit-developer/imagekit-go/internal/testutil" + "github.com/imagekit-developer/imagekit-go/option" +) + +func TestAccountOriginNewWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Accounts.Origins.New(context.TODO(), imagekit.AccountOriginNewParams{ + OriginRequest: imagekit.OriginRequestUnionParam{ + OfS3: &imagekit.OriginRequestS3Param{ + AccessKey: "AKIATEST123", + Bucket: "test-bucket", + Name: "My S3 Origin", + SecretKey: "secrettest123", + BaseURLForCanonicalHeader: imagekit.String("https://cdn.example.com"), + IncludeCanonicalHeader: imagekit.Bool(false), + Prefix: imagekit.String("images"), + }, + }, + }) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestAccountOriginUpdateWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Accounts.Origins.Update( + context.TODO(), + "id", + imagekit.AccountOriginUpdateParams{ + OriginRequest: imagekit.OriginRequestUnionParam{ + OfS3: &imagekit.OriginRequestS3Param{ + AccessKey: "AKIATEST123", + Bucket: "test-bucket", + Name: "My S3 Origin", + SecretKey: "secrettest123", + BaseURLForCanonicalHeader: imagekit.String("https://cdn.example.com"), + IncludeCanonicalHeader: imagekit.Bool(false), + Prefix: imagekit.String("images"), + }, + }, + }, + ) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestAccountOriginList(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Accounts.Origins.List(context.TODO()) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestAccountOriginDelete(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + err := client.Accounts.Origins.Delete(context.TODO(), "id") + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestAccountOriginGet(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Accounts.Origins.Get(context.TODO(), "id") + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/accounturlendpoint.go b/accounturlendpoint.go new file mode 100644 index 0000000..eb281bf --- /dev/null +++ b/accounturlendpoint.go @@ -0,0 +1,426 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/imagekit-developer/imagekit-go/internal/apijson" + shimjson "github.com/imagekit-developer/imagekit-go/internal/encoding/json" + "github.com/imagekit-developer/imagekit-go/internal/requestconfig" + "github.com/imagekit-developer/imagekit-go/option" + "github.com/imagekit-developer/imagekit-go/packages/param" + "github.com/imagekit-developer/imagekit-go/packages/respjson" + "github.com/imagekit-developer/imagekit-go/shared/constant" +) + +// AccountURLEndpointService contains methods and other services that help with +// interacting with the ImageKit API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewAccountURLEndpointService] method instead. +type AccountURLEndpointService struct { + Options []option.RequestOption +} + +// NewAccountURLEndpointService generates a new service that applies the given +// options to each request. These options are applied after the parent client's +// options (if there is one), and before any request-specific options. +func NewAccountURLEndpointService(opts ...option.RequestOption) (r AccountURLEndpointService) { + r = AccountURLEndpointService{} + r.Options = opts + return +} + +// **Note:** This API is currently in beta. +// Creates a new URL‑endpoint and returns the resulting object. +func (r *AccountURLEndpointService) New(ctx context.Context, body AccountURLEndpointNewParams, opts ...option.RequestOption) (res *URLEndpointResponse, err error) { + opts = append(r.Options[:], opts...) + path := "v1/accounts/url-endpoints" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +// **Note:** This API is currently in beta. +// Updates the URL‑endpoint identified by `id` and returns the updated object. +func (r *AccountURLEndpointService) Update(ctx context.Context, id string, body AccountURLEndpointUpdateParams, opts ...option.RequestOption) (res *URLEndpointResponse, err error) { + opts = append(r.Options[:], opts...) + if id == "" { + err = errors.New("missing required id parameter") + return + } + path := fmt.Sprintf("v1/accounts/url-endpoints/%s", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPut, path, body, &res, opts...) + return +} + +// **Note:** This API is currently in beta. +// Returns an array of all URL‑endpoints configured including the default +// URL-endpoint generated by ImageKit during account creation. +func (r *AccountURLEndpointService) List(ctx context.Context, opts ...option.RequestOption) (res *[]URLEndpointResponse, err error) { + opts = append(r.Options[:], opts...) + path := "v1/accounts/url-endpoints" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return +} + +// **Note:** This API is currently in beta. +// Deletes the URL‑endpoint identified by `id`. You cannot delete the default +// URL‑endpoint created by ImageKit during account creation. +func (r *AccountURLEndpointService) Delete(ctx context.Context, id string, opts ...option.RequestOption) (err error) { + opts = append(r.Options[:], opts...) + opts = append([]option.RequestOption{option.WithHeader("Accept", "")}, opts...) + if id == "" { + err = errors.New("missing required id parameter") + return + } + path := fmt.Sprintf("v1/accounts/url-endpoints/%s", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, nil, opts...) + return +} + +// **Note:** This API is currently in beta. +// Retrieves the URL‑endpoint identified by `id`. +func (r *AccountURLEndpointService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *URLEndpointResponse, err error) { + opts = append(r.Options[:], opts...) + if id == "" { + err = errors.New("missing required id parameter") + return + } + path := fmt.Sprintf("v1/accounts/url-endpoints/%s", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return +} + +// Schema for URL endpoint resource. +// +// The property Description is required. +type URLEndpointRequestParam struct { + // Description of the URL endpoint. + Description string `json:"description,required"` + // Path segment appended to your base URL to form the endpoint (letters, digits, + // and hyphens only — or empty for the default endpoint). + URLPrefix param.Opt[string] `json:"urlPrefix,omitzero"` + // Ordered list of origin IDs to try when the file isn’t in the Media Library; + // ImageKit checks them in the sequence provided. Origin must be created before it + // can be used in a URL endpoint. + Origins []string `json:"origins,omitzero"` + // Configuration for third-party URL rewriting. + URLRewriter URLEndpointRequestURLRewriterUnionParam `json:"urlRewriter,omitzero"` + paramObj +} + +func (r URLEndpointRequestParam) MarshalJSON() (data []byte, err error) { + type shadow URLEndpointRequestParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *URLEndpointRequestParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Only one field can be non-zero. +// +// Use [param.IsOmitted] to confirm if a field is set. +type URLEndpointRequestURLRewriterUnionParam struct { + OfCloudinary *URLEndpointRequestURLRewriterCloudinaryParam `json:",omitzero,inline"` + OfImgix *URLEndpointRequestURLRewriterImgixParam `json:",omitzero,inline"` + OfAkamai *URLEndpointRequestURLRewriterAkamaiParam `json:",omitzero,inline"` + paramUnion +} + +func (u URLEndpointRequestURLRewriterUnionParam) MarshalJSON() ([]byte, error) { + return param.MarshalUnion(u, u.OfCloudinary, u.OfImgix, u.OfAkamai) +} +func (u *URLEndpointRequestURLRewriterUnionParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, u) +} + +func (u *URLEndpointRequestURLRewriterUnionParam) asAny() any { + if !param.IsOmitted(u.OfCloudinary) { + return u.OfCloudinary + } else if !param.IsOmitted(u.OfImgix) { + return u.OfImgix + } else if !param.IsOmitted(u.OfAkamai) { + return u.OfAkamai + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u URLEndpointRequestURLRewriterUnionParam) GetPreserveAssetDeliveryTypes() *bool { + if vt := u.OfCloudinary; vt != nil && vt.PreserveAssetDeliveryTypes.Valid() { + return &vt.PreserveAssetDeliveryTypes.Value + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u URLEndpointRequestURLRewriterUnionParam) GetType() *string { + if vt := u.OfCloudinary; vt != nil { + return (*string)(&vt.Type) + } else if vt := u.OfImgix; vt != nil { + return (*string)(&vt.Type) + } else if vt := u.OfAkamai; vt != nil { + return (*string)(&vt.Type) + } + return nil +} + +func init() { + apijson.RegisterUnion[URLEndpointRequestURLRewriterUnionParam]( + "type", + apijson.Discriminator[URLEndpointRequestURLRewriterCloudinaryParam]("CLOUDINARY"), + apijson.Discriminator[URLEndpointRequestURLRewriterImgixParam]("IMGIX"), + apijson.Discriminator[URLEndpointRequestURLRewriterAkamaiParam]("AKAMAI"), + ) +} + +// The property Type is required. +type URLEndpointRequestURLRewriterCloudinaryParam struct { + // Whether to preserve `/` in the rewritten URL. + PreserveAssetDeliveryTypes param.Opt[bool] `json:"preserveAssetDeliveryTypes,omitzero"` + // This field can be elided, and will marshal its zero value as "CLOUDINARY". + Type constant.Cloudinary `json:"type,required"` + paramObj +} + +func (r URLEndpointRequestURLRewriterCloudinaryParam) MarshalJSON() (data []byte, err error) { + type shadow URLEndpointRequestURLRewriterCloudinaryParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *URLEndpointRequestURLRewriterCloudinaryParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +func NewURLEndpointRequestURLRewriterImgixParam() URLEndpointRequestURLRewriterImgixParam { + return URLEndpointRequestURLRewriterImgixParam{ + Type: "IMGIX", + } +} + +// This struct has a constant value, construct it with +// [NewURLEndpointRequestURLRewriterImgixParam]. +type URLEndpointRequestURLRewriterImgixParam struct { + Type constant.Imgix `json:"type,required"` + paramObj +} + +func (r URLEndpointRequestURLRewriterImgixParam) MarshalJSON() (data []byte, err error) { + type shadow URLEndpointRequestURLRewriterImgixParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *URLEndpointRequestURLRewriterImgixParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +func NewURLEndpointRequestURLRewriterAkamaiParam() URLEndpointRequestURLRewriterAkamaiParam { + return URLEndpointRequestURLRewriterAkamaiParam{ + Type: "AKAMAI", + } +} + +// This struct has a constant value, construct it with +// [NewURLEndpointRequestURLRewriterAkamaiParam]. +type URLEndpointRequestURLRewriterAkamaiParam struct { + Type constant.Akamai `json:"type,required"` + paramObj +} + +func (r URLEndpointRequestURLRewriterAkamaiParam) MarshalJSON() (data []byte, err error) { + type shadow URLEndpointRequestURLRewriterAkamaiParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *URLEndpointRequestURLRewriterAkamaiParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// URL‑endpoint object as returned by the API. +type URLEndpointResponse struct { + // Unique identifier for the URL-endpoint. This is generated by ImageKit when you + // create a new URL-endpoint. For the default URL-endpoint, this is always + // `default`. + ID string `json:"id,required"` + // Description of the URL endpoint. + Description string `json:"description,required"` + // Ordered list of origin IDs to try when the file isn’t in the Media Library; + // ImageKit checks them in the sequence provided. Origin must be created before it + // can be used in a URL endpoint. + Origins []string `json:"origins,required"` + // Path segment appended to your base URL to form the endpoint (letters, digits, + // and hyphens only — or empty for the default endpoint). + URLPrefix string `json:"urlPrefix,required"` + // Configuration for third-party URL rewriting. + URLRewriter URLEndpointResponseURLRewriterUnion `json:"urlRewriter"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ID respjson.Field + Description respjson.Field + Origins respjson.Field + URLPrefix respjson.Field + URLRewriter respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r URLEndpointResponse) RawJSON() string { return r.JSON.raw } +func (r *URLEndpointResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// URLEndpointResponseURLRewriterUnion contains all possible properties and values +// from [URLEndpointResponseURLRewriterCloudinary], +// [URLEndpointResponseURLRewriterImgix], [URLEndpointResponseURLRewriterAkamai]. +// +// Use the [URLEndpointResponseURLRewriterUnion.AsAny] method to switch on the +// variant. +// +// Use the methods beginning with 'As' to cast the union to one of its variants. +type URLEndpointResponseURLRewriterUnion struct { + // This field is from variant [URLEndpointResponseURLRewriterCloudinary]. + PreserveAssetDeliveryTypes bool `json:"preserveAssetDeliveryTypes"` + // Any of "CLOUDINARY", "IMGIX", "AKAMAI". + Type string `json:"type"` + JSON struct { + PreserveAssetDeliveryTypes respjson.Field + Type respjson.Field + raw string + } `json:"-"` +} + +// anyURLEndpointResponseURLRewriter is implemented by each variant of +// [URLEndpointResponseURLRewriterUnion] to add type safety for the return type of +// [URLEndpointResponseURLRewriterUnion.AsAny] +type anyURLEndpointResponseURLRewriter interface { + implURLEndpointResponseURLRewriterUnion() +} + +func (URLEndpointResponseURLRewriterCloudinary) implURLEndpointResponseURLRewriterUnion() {} +func (URLEndpointResponseURLRewriterImgix) implURLEndpointResponseURLRewriterUnion() {} +func (URLEndpointResponseURLRewriterAkamai) implURLEndpointResponseURLRewriterUnion() {} + +// Use the following switch statement to find the correct variant +// +// switch variant := URLEndpointResponseURLRewriterUnion.AsAny().(type) { +// case imagekit.URLEndpointResponseURLRewriterCloudinary: +// case imagekit.URLEndpointResponseURLRewriterImgix: +// case imagekit.URLEndpointResponseURLRewriterAkamai: +// default: +// fmt.Errorf("no variant present") +// } +func (u URLEndpointResponseURLRewriterUnion) AsAny() anyURLEndpointResponseURLRewriter { + switch u.Type { + case "CLOUDINARY": + return u.AsCloudinary() + case "IMGIX": + return u.AsImgix() + case "AKAMAI": + return u.AsAkamai() + } + return nil +} + +func (u URLEndpointResponseURLRewriterUnion) AsCloudinary() (v URLEndpointResponseURLRewriterCloudinary) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u URLEndpointResponseURLRewriterUnion) AsImgix() (v URLEndpointResponseURLRewriterImgix) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u URLEndpointResponseURLRewriterUnion) AsAkamai() (v URLEndpointResponseURLRewriterAkamai) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +// Returns the unmodified JSON received from the API +func (u URLEndpointResponseURLRewriterUnion) RawJSON() string { return u.JSON.raw } + +func (r *URLEndpointResponseURLRewriterUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type URLEndpointResponseURLRewriterCloudinary struct { + // Whether to preserve `/` in the rewritten URL. + PreserveAssetDeliveryTypes bool `json:"preserveAssetDeliveryTypes,required"` + Type constant.Cloudinary `json:"type,required"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + PreserveAssetDeliveryTypes respjson.Field + Type respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r URLEndpointResponseURLRewriterCloudinary) RawJSON() string { return r.JSON.raw } +func (r *URLEndpointResponseURLRewriterCloudinary) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type URLEndpointResponseURLRewriterImgix struct { + Type constant.Imgix `json:"type,required"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Type respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r URLEndpointResponseURLRewriterImgix) RawJSON() string { return r.JSON.raw } +func (r *URLEndpointResponseURLRewriterImgix) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type URLEndpointResponseURLRewriterAkamai struct { + Type constant.Akamai `json:"type,required"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Type respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r URLEndpointResponseURLRewriterAkamai) RawJSON() string { return r.JSON.raw } +func (r *URLEndpointResponseURLRewriterAkamai) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type AccountURLEndpointNewParams struct { + // Schema for URL endpoint resource. + URLEndpointRequest URLEndpointRequestParam + paramObj +} + +func (r AccountURLEndpointNewParams) MarshalJSON() (data []byte, err error) { + return shimjson.Marshal(r.URLEndpointRequest) +} +func (r *AccountURLEndpointNewParams) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &r.URLEndpointRequest) +} + +type AccountURLEndpointUpdateParams struct { + // Schema for URL endpoint resource. + URLEndpointRequest URLEndpointRequestParam + paramObj +} + +func (r AccountURLEndpointUpdateParams) MarshalJSON() (data []byte, err error) { + return shimjson.Marshal(r.URLEndpointRequest) +} +func (r *AccountURLEndpointUpdateParams) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &r.URLEndpointRequest) +} diff --git a/accounturlendpoint_test.go b/accounturlendpoint_test.go new file mode 100644 index 0000000..130b748 --- /dev/null +++ b/accounturlendpoint_test.go @@ -0,0 +1,160 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/imagekit-developer/imagekit-go" + "github.com/imagekit-developer/imagekit-go/internal/testutil" + "github.com/imagekit-developer/imagekit-go/option" +) + +func TestAccountURLEndpointNewWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Accounts.URLEndpoints.New(context.TODO(), imagekit.AccountURLEndpointNewParams{ + URLEndpointRequest: imagekit.URLEndpointRequestParam{ + Description: "My custom URL endpoint", + Origins: []string{"origin-id-1"}, + URLPrefix: imagekit.String("product-images"), + URLRewriter: imagekit.URLEndpointRequestURLRewriterUnionParam{ + OfCloudinary: &imagekit.URLEndpointRequestURLRewriterCloudinaryParam{ + PreserveAssetDeliveryTypes: imagekit.Bool(true), + }, + }, + }, + }) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestAccountURLEndpointUpdateWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Accounts.URLEndpoints.Update( + context.TODO(), + "id", + imagekit.AccountURLEndpointUpdateParams{ + URLEndpointRequest: imagekit.URLEndpointRequestParam{ + Description: "My custom URL endpoint", + Origins: []string{"origin-id-1"}, + URLPrefix: imagekit.String("product-images"), + URLRewriter: imagekit.URLEndpointRequestURLRewriterUnionParam{ + OfCloudinary: &imagekit.URLEndpointRequestURLRewriterCloudinaryParam{ + PreserveAssetDeliveryTypes: imagekit.Bool(true), + }, + }, + }, + }, + ) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestAccountURLEndpointList(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Accounts.URLEndpoints.List(context.TODO()) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestAccountURLEndpointDelete(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + err := client.Accounts.URLEndpoints.Delete(context.TODO(), "id") + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestAccountURLEndpointGet(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Accounts.URLEndpoints.Get(context.TODO(), "id") + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/accountusage.go b/accountusage.go new file mode 100644 index 0000000..8a1b7b8 --- /dev/null +++ b/accountusage.go @@ -0,0 +1,93 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit + +import ( + "context" + "net/http" + "net/url" + "time" + + "github.com/imagekit-developer/imagekit-go/internal/apijson" + "github.com/imagekit-developer/imagekit-go/internal/apiquery" + "github.com/imagekit-developer/imagekit-go/internal/requestconfig" + "github.com/imagekit-developer/imagekit-go/option" + "github.com/imagekit-developer/imagekit-go/packages/respjson" +) + +// AccountUsageService contains methods and other services that help with +// interacting with the ImageKit API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewAccountUsageService] method instead. +type AccountUsageService struct { + Options []option.RequestOption +} + +// NewAccountUsageService generates a new service that applies the given options to +// each request. These options are applied after the parent client's options (if +// there is one), and before any request-specific options. +func NewAccountUsageService(opts ...option.RequestOption) (r AccountUsageService) { + r = AccountUsageService{} + r.Options = opts + return +} + +// Get the account usage information between two dates. Note that the API response +// includes data from the start date while excluding data from the end date. In +// other words, the data covers the period starting from the specified start date +// up to, but not including, the end date. +func (r *AccountUsageService) Get(ctx context.Context, query AccountUsageGetParams, opts ...option.RequestOption) (res *AccountUsageGetResponse, err error) { + opts = append(r.Options[:], opts...) + path := "v1/accounts/usage" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) + return +} + +type AccountUsageGetResponse struct { + // Amount of bandwidth used in bytes. + BandwidthBytes int64 `json:"bandwidthBytes"` + // Number of extension units used. + ExtensionUnitsCount int64 `json:"extensionUnitsCount"` + // Storage used by media library in bytes. + MediaLibraryStorageBytes int64 `json:"mediaLibraryStorageBytes"` + // Storage used by the original cache in bytes. + OriginalCacheStorageBytes int64 `json:"originalCacheStorageBytes"` + // Number of video processing units used. + VideoProcessingUnitsCount int64 `json:"videoProcessingUnitsCount"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + BandwidthBytes respjson.Field + ExtensionUnitsCount respjson.Field + MediaLibraryStorageBytes respjson.Field + OriginalCacheStorageBytes respjson.Field + VideoProcessingUnitsCount respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r AccountUsageGetResponse) RawJSON() string { return r.JSON.raw } +func (r *AccountUsageGetResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type AccountUsageGetParams struct { + // Specify a `endDate` in `YYYY-MM-DD` format. It should be after the `startDate`. + // The difference between `startDate` and `endDate` should be less than 90 days. + EndDate time.Time `query:"endDate,required" format:"date" json:"-"` + // Specify a `startDate` in `YYYY-MM-DD` format. It should be before the `endDate`. + // The difference between `startDate` and `endDate` should be less than 90 days. + StartDate time.Time `query:"startDate,required" format:"date" json:"-"` + paramObj +} + +// URLQuery serializes [AccountUsageGetParams]'s query parameters as `url.Values`. +func (r AccountUsageGetParams) URLQuery() (v url.Values, err error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} diff --git a/accountusage_test.go b/accountusage_test.go new file mode 100644 index 0000000..d2dfecd --- /dev/null +++ b/accountusage_test.go @@ -0,0 +1,42 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit_test + +import ( + "context" + "errors" + "os" + "testing" + "time" + + "github.com/imagekit-developer/imagekit-go" + "github.com/imagekit-developer/imagekit-go/internal/testutil" + "github.com/imagekit-developer/imagekit-go/option" +) + +func TestAccountUsageGet(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Accounts.Usage.Get(context.TODO(), imagekit.AccountUsageGetParams{ + EndDate: time.Now(), + StartDate: time.Now(), + }) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/aliases.go b/aliases.go new file mode 100644 index 0000000..f751417 --- /dev/null +++ b/aliases.go @@ -0,0 +1,38 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit + +import ( + "github.com/imagekit-developer/imagekit-go/internal/apierror" + "github.com/imagekit-developer/imagekit-go/packages/param" + "github.com/imagekit-developer/imagekit-go/shared" +) + +// aliased to make [param.APIUnion] private when embedding +type paramUnion = param.APIUnion + +// aliased to make [param.APIObject] private when embedding +type paramObj = param.APIObject + +type Error = apierror.Error + +// Array of extensions to be applied to the asset. Each extension can be configured +// with specific parameters based on the extension type. +// +// This is an alias to an internal type. +type ExtensionsParam = shared.ExtensionsParam + +// This is an alias to an internal type. +type ExtensionUnionParam = shared.ExtensionUnionParam + +// This is an alias to an internal type. +type ExtensionRemoveBgParam = shared.ExtensionRemoveBgParam + +// This is an alias to an internal type. +type ExtensionRemoveBgOptionsParam = shared.ExtensionRemoveBgOptionsParam + +// This is an alias to an internal type. +type ExtensionAutoTaggingParam = shared.ExtensionAutoTaggingParam + +// This is an alias to an internal type. +type ExtensionAIAutoDescriptionParam = shared.ExtensionAIAutoDescriptionParam diff --git a/api.md b/api.md new file mode 100644 index 0000000..77d10df --- /dev/null +++ b/api.md @@ -0,0 +1,209 @@ +# Shared Params Types + +- shared.ExtensionsParam + +# CustomMetadataFields + +Response Types: + +- imagekit.CustomMetadataField +- imagekit.CustomMetadataFieldDeleteResponse + +Methods: + +- client.CustomMetadataFields.New(ctx context.Context, body imagekit.CustomMetadataFieldNewParams) (imagekit.CustomMetadataField, error) +- client.CustomMetadataFields.Update(ctx context.Context, id string, body imagekit.CustomMetadataFieldUpdateParams) (imagekit.CustomMetadataField, error) +- client.CustomMetadataFields.List(ctx context.Context, query imagekit.CustomMetadataFieldListParams) ([]imagekit.CustomMetadataField, error) +- client.CustomMetadataFields.Delete(ctx context.Context, id string) (imagekit.CustomMetadataFieldDeleteResponse, error) + +# Files + +Params Types: + +- imagekit.UpdateFileDetailsRequestUnionParam + +Response Types: + +- imagekit.File +- imagekit.Folder +- imagekit.Metadata +- imagekit.FileUpdateResponse +- imagekit.FileCopyResponse +- imagekit.FileMoveResponse +- imagekit.FileRenameResponse +- imagekit.FileUploadResponse + +Methods: + +- client.Files.Update(ctx context.Context, fileID string, body imagekit.FileUpdateParams) (imagekit.FileUpdateResponse, error) +- client.Files.Delete(ctx context.Context, fileID string) error +- client.Files.Copy(ctx context.Context, body imagekit.FileCopyParams) (imagekit.FileCopyResponse, error) +- client.Files.Get(ctx context.Context, fileID string) (imagekit.File, error) +- client.Files.Move(ctx context.Context, body imagekit.FileMoveParams) (imagekit.FileMoveResponse, error) +- client.Files.Rename(ctx context.Context, body imagekit.FileRenameParams) (imagekit.FileRenameResponse, error) +- client.Files.Upload(ctx context.Context, body imagekit.FileUploadParams) (imagekit.FileUploadResponse, error) + +## Bulk + +Response Types: + +- imagekit.FileBulkDeleteResponse +- imagekit.FileBulkAddTagsResponse +- imagekit.FileBulkRemoveAITagsResponse +- imagekit.FileBulkRemoveTagsResponse + +Methods: + +- client.Files.Bulk.Delete(ctx context.Context, body imagekit.FileBulkDeleteParams) (imagekit.FileBulkDeleteResponse, error) +- client.Files.Bulk.AddTags(ctx context.Context, body imagekit.FileBulkAddTagsParams) (imagekit.FileBulkAddTagsResponse, error) +- client.Files.Bulk.RemoveAITags(ctx context.Context, body imagekit.FileBulkRemoveAITagsParams) (imagekit.FileBulkRemoveAITagsResponse, error) +- client.Files.Bulk.RemoveTags(ctx context.Context, body imagekit.FileBulkRemoveTagsParams) (imagekit.FileBulkRemoveTagsResponse, error) + +## Versions + +Response Types: + +- imagekit.FileVersionDeleteResponse + +Methods: + +- client.Files.Versions.List(ctx context.Context, fileID string) ([]imagekit.File, error) +- client.Files.Versions.Delete(ctx context.Context, versionID string, body imagekit.FileVersionDeleteParams) (imagekit.FileVersionDeleteResponse, error) +- client.Files.Versions.Get(ctx context.Context, versionID string, query imagekit.FileVersionGetParams) (imagekit.File, error) +- client.Files.Versions.Restore(ctx context.Context, versionID string, body imagekit.FileVersionRestoreParams) (imagekit.File, error) + +## Metadata + +Methods: + +- client.Files.Metadata.Get(ctx context.Context, fileID string) (imagekit.Metadata, error) +- client.Files.Metadata.GetFromURL(ctx context.Context, query imagekit.FileMetadataGetFromURLParams) (imagekit.Metadata, error) + +# Assets + +Response Types: + +- imagekit.AssetListResponseUnion + +Methods: + +- client.Assets.List(ctx context.Context, query imagekit.AssetListParams) ([]imagekit.AssetListResponseUnion, error) + +# Cache + +## Invalidation + +Response Types: + +- imagekit.CacheInvalidationNewResponse +- imagekit.CacheInvalidationGetResponse + +Methods: + +- client.Cache.Invalidation.New(ctx context.Context, body imagekit.CacheInvalidationNewParams) (imagekit.CacheInvalidationNewResponse, error) +- client.Cache.Invalidation.Get(ctx context.Context, requestID string) (imagekit.CacheInvalidationGetResponse, error) + +# Folders + +Response Types: + +- imagekit.FolderNewResponse +- imagekit.FolderDeleteResponse +- imagekit.FolderCopyResponse +- imagekit.FolderMoveResponse +- imagekit.FolderRenameResponse + +Methods: + +- client.Folders.New(ctx context.Context, body imagekit.FolderNewParams) (imagekit.FolderNewResponse, error) +- client.Folders.Delete(ctx context.Context, body imagekit.FolderDeleteParams) (imagekit.FolderDeleteResponse, error) +- client.Folders.Copy(ctx context.Context, body imagekit.FolderCopyParams) (imagekit.FolderCopyResponse, error) +- client.Folders.Move(ctx context.Context, body imagekit.FolderMoveParams) (imagekit.FolderMoveResponse, error) +- client.Folders.Rename(ctx context.Context, body imagekit.FolderRenameParams) (imagekit.FolderRenameResponse, error) + +## Job + +Response Types: + +- imagekit.FolderJobGetResponse + +Methods: + +- client.Folders.Job.Get(ctx context.Context, jobID string) (imagekit.FolderJobGetResponse, error) + +# Accounts + +## Usage + +Response Types: + +- imagekit.AccountUsageGetResponse + +Methods: + +- client.Accounts.Usage.Get(ctx context.Context, query imagekit.AccountUsageGetParams) (imagekit.AccountUsageGetResponse, error) + +## Origins + +Params Types: + +- imagekit.OriginRequestUnionParam + +Response Types: + +- imagekit.OriginResponseUnion + +Methods: + +- client.Accounts.Origins.New(ctx context.Context, body imagekit.AccountOriginNewParams) (imagekit.OriginResponseUnion, error) +- client.Accounts.Origins.Update(ctx context.Context, id string, body imagekit.AccountOriginUpdateParams) (imagekit.OriginResponseUnion, error) +- client.Accounts.Origins.List(ctx context.Context) ([]imagekit.OriginResponseUnion, error) +- client.Accounts.Origins.Delete(ctx context.Context, id string) error +- client.Accounts.Origins.Get(ctx context.Context, id string) (imagekit.OriginResponseUnion, error) + +## URLEndpoints + +Params Types: + +- imagekit.URLEndpointRequestParam + +Response Types: + +- imagekit.URLEndpointResponse + +Methods: + +- client.Accounts.URLEndpoints.New(ctx context.Context, body imagekit.AccountURLEndpointNewParams) (imagekit.URLEndpointResponse, error) +- client.Accounts.URLEndpoints.Update(ctx context.Context, id string, body imagekit.AccountURLEndpointUpdateParams) (imagekit.URLEndpointResponse, error) +- client.Accounts.URLEndpoints.List(ctx context.Context) ([]imagekit.URLEndpointResponse, error) +- client.Accounts.URLEndpoints.Delete(ctx context.Context, id string) error +- client.Accounts.URLEndpoints.Get(ctx context.Context, id string) (imagekit.URLEndpointResponse, error) + +# Beta + +## V2 + +### Files + +Response Types: + +- imagekit.BetaV2FileUploadResponse + +Methods: + +- client.Beta.V2.Files.Upload(ctx context.Context, body imagekit.BetaV2FileUploadParams) (imagekit.BetaV2FileUploadResponse, error) + +# Webhooks + +Response Types: + +- imagekit.BaseWebhookEvent +- imagekit.UploadPostTransformErrorEvent +- imagekit.UploadPostTransformSuccessEvent +- imagekit.UploadPreTransformErrorEvent +- imagekit.UploadPreTransformSuccessEvent +- imagekit.VideoTransformationAcceptedEvent +- imagekit.VideoTransformationErrorEvent +- imagekit.VideoTransformationReadyEvent +- imagekit.UnsafeUnwrapWebhookEventUnion +- imagekit.UnwrapWebhookEventUnion diff --git a/api/api.go b/api/api.go deleted file mode 100644 index 7ba1a35..0000000 --- a/api/api.go +++ /dev/null @@ -1,229 +0,0 @@ -package api - -import ( - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "net/url" - "reflect" - "regexp" - "strconv" - "strings" -) - -// HttpClient interface to provide Do(req *http.Request) method -type HttpClient interface { - Do(req *http.Request) (*http.Response, error) -} - -// ResponseMetaData is used in response objects to provide metadata -type ResponseMetaData struct { - Header http.Header - StatusCode int - Body []byte -} - -// Stringer to get printable metadata -func (rm ResponseMetaData) String() string { - return fmt.Sprintf("%d\n%s\n%v", rm.StatusCode, string(rm.Body), rm.Header) -} - -// Response is promoted struct to response objects -type Response struct { - ResponseMetaData -} - -// SetMeta method assigns given metadata -func (resp *Response) SetMeta(meta ResponseMetaData) { - resp.ResponseMetaData = meta -} - -// Body returns raw http response body -func (resp *Response) Body() []byte { - return resp.ResponseMetaData.Body -} - -// ParseError returns error object by parsing the http response body if applicable otherwise returns core error such as ErrUnauthorized, ErrServer etc. -func (resp *Response) ParseError() error { - var err error - var code = resp.ResponseMetaData.StatusCode - - if code > 199 && code < 300 { - return nil - } - - switch code { - case 400: - err = ParseError(resp.ResponseMetaData.Body, ErrBadRequest) - case 401: - return ErrUnauthorized - case 403: - err = ParseError(resp.ResponseMetaData.Body, ErrForbidden) - case 404: - err = ErrNotFound - case 429: - err = ErrTooManyRequests - case 500, 502, 503, 504: - err = ErrServer - default: - err = ErrUndefined - } - return err -} - -type ApiError struct { - Message string `json:"message"` - Reason string `json:"reason"` - Errors map[string]string `json:"errors"` - err error `json:"-"` -} - -func (e ApiError) Error() string { - return e.Message -} - -func (e ApiError) Unwrap() error { - return e.err -} - -func ParseError(body []byte, embed error) error { - var ikError = &ApiError{} - - err := json.Unmarshal(body, ikError) - if err != nil { - return err - } - - ikError.err = embed - return ikError -} - -// MetaSetter is an interface to provide type safety to set meta -type MetaSetter interface { - ParseError() error - SetMeta(ResponseMetaData) -} - -// base64DataRegex is the regular expression for detecting base64 encoded strings. -var base64DataRegex = regexp.MustCompile("^data:([\\w-]+/[\\w\\-+.]+)?(;[\\w-]+=[\\w-]+)*;base64,([a-zA-Z0-9/+\\n=]+)$") - -// StructToParams serializes struct to url.Values, which can be further sent to the http client. -func StructToParams(inputStruct interface{}) (url.Values, error) { - var paramsMap map[string]interface{} - paramsJSONObj, _ := json.Marshal(inputStruct) - err := json.Unmarshal(paramsJSONObj, ¶msMap) - if err != nil { - return nil, err - } - - params := url.Values{} - for paramName, value := range paramsMap { - kind := reflect.ValueOf(value).Kind() - - if kind == reflect.Slice || kind == reflect.Array { - rVal := reflect.ValueOf(value) - for i := 0; i < rVal.Len(); i++ { - item := rVal.Index(i) - val, err := encodeParamValue(item.Interface()) - if err != nil { - return nil, err - } - - arrParamName := fmt.Sprintf("%s[%d]", paramName, i) - params.Add(arrParamName, val) - } - - continue - } - - val, err := encodeParamValue(value) - if err != nil { - return nil, err - } - - params.Add(paramName, val) - } - - return params, nil -} - -func encodeParamValue(value interface{}) (string, error) { - resBytes, err := json.Marshal(value) - if err != nil { - return "", err - } - - res := string(resBytes) - if strings.HasPrefix(res, "\"") { // FIXME: Fix this dirty hack that prevents double quoting of strings - res, _ = strconv.Unquote(res) - } - - return res, nil -} - -// BuildPath builds (joins) the URL path from the provided parts. -func BuildPath(parts ...interface{}) string { - var partsSlice []string - - for _, part := range parts { - partRes := "" - switch partVal := part.(type) { - case string: - partRes = partVal - case fmt.Stringer: - partRes = partVal.String() - default: - partRes = fmt.Sprintf("%v", partVal) - } - if len(partRes) > 0 { - partsSlice = append(partsSlice, strings.Trim(partRes, "/")) - } - } - - return strings.Join(partsSlice, "/") -} - -// DeferredClose is a wrapper around io.Closer.Close method. -func DeferredClose(c io.Closer) { - if err := c.Close(); err != nil { - log.Println(err) - } -} - -// DeferredBodyClose closes http response body -func DeferredBodyClose(resp *http.Response) { - if resp != nil { - DeferredClose(resp.Body) - } -} - -// SetResponseMeta assigns given http response data to response objects -func SetResponseMeta(httpResp *http.Response, respStruct MetaSetter) { - if httpResp == nil { - return - } - - meta := ResponseMetaData{ - Header: httpResp.Header, - StatusCode: httpResp.StatusCode, - } - - if body, err := io.ReadAll(httpResp.Body); err == nil { - meta.Body = body - } - respStruct.SetMeta(meta) -} - -func Bool(b bool) *bool { - return &b -} - -func Int(i int) *int { - return &i -} - -func Float32(f float32) *float32 { - return &f -} diff --git a/api/api_test.go b/api/api_test.go deleted file mode 100644 index 0c52275..0000000 --- a/api/api_test.go +++ /dev/null @@ -1,257 +0,0 @@ -package api - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "strings" - "testing" - - "github.com/google/go-cmp/cmp" -) - -type Tstringer struct { - name string -} - -func (ts Tstringer) String() string { - return ts.name -} - -// read closer implementation -type MockedRC struct { - called bool - data io.ReadCloser -} - -func (rc *MockedRC) Close() error { - rc.called = true - return nil -} - -func (*MockedRC) Read(p []byte) (n int, err error) { - return n, err -} - -type MockedResponse struct { - ResponseMetaData -} - -func (resp *MockedResponse) SetMeta(meta ResponseMetaData) { - resp.ResponseMetaData = meta -} - -func (resp *MockedResponse) ParseError() error { - return nil -} - -func Test_SetMeta(t *testing.T) { - resp := &Response{} - meta := ResponseMetaData{} - - resp.SetMeta(meta) - - if !cmp.Equal(resp.ResponseMetaData, meta) { - t.Error("ResponseMetaData set") - } -} - -func Test_Body(t *testing.T) { - b := []byte("test") - - resp := &Response{ - ResponseMetaData{ - Body: b, - }, - } - - if !cmp.Equal(resp.Body(), b) { - t.Error("invalid body") - } -} - -func Test_ParseError(t *testing.T) { - h := http.Header{"content-type": []string{"application/json"}} - - type Ctype struct { - code int - result error - } - var cases = map[string]Ctype{ - "nill": { - 200, - nil, - }, - "bad request": { - 400, - ErrBadRequest, - }, - "unauthorized": { - 401, - ErrUnauthorized, - }, - "Forbidden": { - 403, - ErrForbidden, - }, - "not-found": { - 404, - ErrNotFound, - }, - "too-many-requests": { - 429, - ErrTooManyRequests, - }, - "undefine err": { - 600, - ErrUndefined, - }, - } - - for _, c := range []int{500, 502, 503, 504} { - cases[fmt.Sprintf("server error %d", c)] = Ctype{ - c, - ErrServer, - } - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - resp := &Response{ - ResponseMetaData{ - Header: h, - Body: []byte(`{"message":"test error","reason":"test reason"}`), - StatusCode: tc.code, - }, - } - - err := resp.ParseError() - - if !errors.Is(err, tc.result) { - t.Error("expected: " + tc.result.Error() + "\n got: " + err.Error()) - } - }) - } - -} - -func Test_StructtoParams(t *testing.T) { - var cases = map[string]struct { - input any - result string - err error - }{ - "test list": { - input: struct { - List []string - }{[]string{"one", "two"}}, - result: `{"List[0]":["one"],"List[1]":["two"]}`, - }, - "test array": { - input: struct { - List [2]string - }{[2]string{"one", "two"}}, - result: `{"List[0]":["one"],"List[1]":["two"]}`, - }, - "scalar": { - input: struct { - Rank int - Name string - }{2, "test"}, - result: `{"Name":["test"],"Rank":["2"]}`, - }, - "should fail": { - input: "abc", - result: "", - err: errors.New("json: cannot unmarshal string into Go value of type map[string]interface {}"), - }, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - res, err := StructToParams(tc.input) - - if err != nil && tc.err == nil { - t.Fatal(err) - } - - if tc.err != nil { - if err == nil { - t.Error("err is nil") - } - if err.Error() != tc.err.Error() { - t.Error("invalid error") - } - return - } - str, err := json.Marshal(res) - - if string(str) != tc.result { - t.Error("expected: " + tc.result + "\ngot: " + string(str)) - } - }) - } -} - -func Test_BuildPath(t *testing.T) { - - parts := []any{ - 1, "two", Tstringer{"test"}, - } - - result := BuildPath(parts...) - - if result != "1/two/test" { - t.Error("invalid") - } -} - -func Test_DeferredBodyClose(t *testing.T) { - var rc = &MockedRC{} - - var resp = &http.Response{ - Body: rc, - } - - DeferredBodyClose(resp) - if rc.called == false { - t.Error("deffered close not called") - } -} - -func Test_SetResponseMeta(t *testing.T) { - h := http.Header{"content-type": []string{"application/json"}} - - var response = &MockedResponse{} - var meta = ResponseMetaData{ - Header: h, - StatusCode: 200, - Body: []byte("hello"), - } - - SetResponseMeta(&http.Response{ - Header: h, - Body: io.NopCloser(strings.NewReader("hello")), - StatusCode: 200, - }, response) - - if !cmp.Equal(response.ResponseMetaData, meta) { - t.Error("invalid header") - } -} - -func Test_Bool(t *testing.T) { - resp := Bool(true) - - if *resp != true { - t.Error("invalid bool(true)") - } - - resp = Bool(false) - - if *resp != false { - t.Error("invalid bool(false)") - } -} diff --git a/api/errors.go b/api/errors.go deleted file mode 100644 index 2a3d274..0000000 --- a/api/errors.go +++ /dev/null @@ -1,11 +0,0 @@ -package api - -import "errors" - -var ErrBadRequest = errors.New("Bad Request") -var ErrForbidden = errors.New("Forbidden") -var ErrUnauthorized = errors.New("Unauthorized") -var ErrTooManyRequests = errors.New("Too Many Requests") -var ErrServer = errors.New("Server Error") -var ErrNotFound = errors.New("Not Found") -var ErrUndefined = errors.New("Undefined Error") diff --git a/api/extension/extension.go b/api/extension/extension.go deleted file mode 100644 index 6c110ac..0000000 --- a/api/extension/extension.go +++ /dev/null @@ -1,60 +0,0 @@ -package extension - -type TagService string - -const ( - GoogleAutoTag TagService = "google-auto-tagging" - AwsAutoTag TagService = "aws-auto-tagging" -) - -// RemoveBgOptions represents different options for removing bg extension -type RemoveBgOption struct { - AddShadow bool `json:"add_shadow"` - SemiTransparency bool `json:"semitransparency"` - BgColor string `json:"bg_color,omitempty"` - BgImageUrl string `json:"bg_image_url,omitempty"` -} - -// AutoTag represents extension struct for auto tagging by providers such as google and aws -type AutoTag struct { - Name TagService `json:"name"` - MinConfidence int `json:"minConfidence"` - MaxTags int `json:"maxTags,omitempty"` -} - -func (e *AutoTag) extName() string { - return string(e.Name) -} - -// NewAutoTag creates an extension parameter for auto tagging -func NewAutoTag(service TagService, minConf int, maxTags int) *AutoTag { - return &AutoTag{ - Name: service, - MinConfidence: minConf, - MaxTags: maxTags, - } -} - -// RemoveBg represents extension struct for removing background -type RemoveBg struct { - Name string `json:"name"` - Options RemoveBgOption `json:"options,omitempty"` -} - -func (e *RemoveBg) extName() string { - return e.Name -} - -// NewRemoveBg creates an extension parameter for removing background -func NewRemoveBg(opt RemoveBgOption) *RemoveBg { - return &RemoveBg{ - Name: "remove-bg", - Options: opt, - } -} - -// IExtension prepresents common interface for different extensions. -// This interface is used only as a common type to store different types of extensions in a slice or array. -type IExtension interface { - extName() string -} diff --git a/api/media/assets.go b/api/media/assets.go deleted file mode 100644 index a864a2d..0000000 --- a/api/media/assets.go +++ /dev/null @@ -1,600 +0,0 @@ -package media - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "strings" - "time" - - "github.com/imagekit-developer/imagekit-go/api" - "github.com/imagekit-developer/imagekit-go/api/extension" - "gopkg.in/validator.v2" -) - -// ListType represents type of media library files in request filter. -type ListType string - -const ( - ListFile ListType = "file" - ListFTFileVersion ListType = "file-version" - ListFolder ListType = "folder" -) - -// Sort specifies sort order for ListFiles results data. -type Sort string - -const ( - AscName Sort = "ASC_NAME" - DescName Sort = "DESC_NAME" - AscCreated Sort = "ASC_CREATED" - DescCreated Sort = "DESC_CREATED" - AscUpdated Sort = "ASC_UPDATED" - DescUpdated Sort = "DESC_UPDATED" - AscHeight Sort = "ASC_HEIGHT" - DescHeight Sort = "DESC_HEIGHT" - AscWidth Sort = "ASC_WIDTH" - DescWidth Sort = "DESC_WIDTH" - AscSize Sort = "ASC_SIZE" - DescSize Sort = "DESC_SIZE" -) - -// FileType represents all, image or non-image etc type in request filter. -type FileType string - -const ( - All FileType = "all" - Image FileType = "image" - NonImage FileType = "non-image" -) - -// FilesParam struct is a parameter type to ListFiles() function to search / list media library files. -type FilesParam struct { - Type ListType `json:"type,omitempty"` - Sort Sort `json:"sort,omitempty"` - Path string `json:"path,omitempty"` - SearchQuery string `json:"searchQuery,omitempty"` - FileType FileType `json:"fileType,omitempty"` - Tags string `json:"tags,omitempty"` - Limit int `json:"limit,omitempty"` - Skip int `json:"skip,omitempty"` -} - -// FileVersionsParam represents filter for getting file's version -type FileVersionsParam struct { - FileId string `validate:"nonzero" json:"fileId"` - VersionId string `json:"versionId,omitempty"` -} - -// File represents media library File details. -type File struct { - FileId string `json:"fileId"` - Name string `json:"name"` - FilePath string `json:"filePath"` - Tags []string - AITags []map[string]any `json:"AITags"` - VersionInfo map[string]string `json:"versionInfo"` - IsPrivateFile *bool `json:"isPrivateFile"` - CustomCoordinates *string `json:"customCoordinates"` - Url string `json:"url"` - Thumbnail string `json:"thumbnail"` - FileType FileType `json:"fileType"` - Mime string `json:"mime"` - Height int `json:"height"` - Width int `json:"Width"` - Size uint64 `json:"size"` - HasAlpha bool `json:"hasAlpha"` - CustomMetadata map[string]any `json:"customMetadata,omitempty"` - EmbeddedMetadata map[string]any `json:"embeddedMetadata"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` -} - -// FilesResponse represents response type of Files(). -type FilesResponse struct { - Data []File - api.Response -} - -// FileResponse represents response type of FileById(). -type FileResponse struct { - Data File - api.Response -} - -// UpdateFileParam represents file attributes to update -type UpdateFileParam struct { - RemoveAITags []string `json:"removeAITags,omitempty"` - WebhookUrl string `json:"webhookUrl,omitempty"` - Extensions []extension.IExtension `json:"extensions,omitempty"` - Tags []string `json:"tags,omitempty"` - CustomCoordinates string `json:"customCoordinates,omitempty"` - CustomMetadata map[string]any `json:"customMetadata,omitempty"` -} - -// TagsParam represents parameters to add tags to bulk files -type TagsParam struct { - FileIds []string `json:"fileIds"` - Tags []string `json:"tags"` -} - -// AITagsParam represents parameters to add AI tags to bulk files -type AITagsParam struct { - FileIds []string `json:"fileIds"` - AITags []string `json:"AITags"` -} - -// UpdatedIds represents response to tags update calls -type UpdatedIds struct { - FileIds []string `json:"successfullyUpdatedFileIds"` -} - -// TagsResponse represents response to add tags to bulk files. Contains fileIds in Data -type TagsResponse struct { - Data UpdatedIds - api.Response -} - -// FileIdsParam is a struct to hold slice of file ids to pass as a parameter. -type FileIdsParam struct { - FileIds []string `validate:"nonzero" json:"fileIds"` -} - -// DeletedIds is a struct to hold slice of successfully deleted files ids. -type DeletedIds struct { - FileIds []string `json:"successfullyDeletedFileIds"` - Errors []map[string]string `json:"errors"` -} - -// DeleteFilessResponse represents response to delete files api which includes ids of deleted files. -type DeleteFilesResponse struct { - Data DeletedIds - api.Response -} - -// CopyFileParam represents parameters to copy files api -type CopyFileParam struct { - SourcePath string `validate:"nonzero" json:"sourceFilePath"` - DestinationPath string `validate:"nonzero" json:"destinationPath"` - IncludeFileVersions bool `json:"includeFileVersions"` -} - -// MoveFileParam represents parameters to move file api -type MoveFileParam struct { - SourcePath string `validate:"nonzero" json:"sourceFilePath"` - DestinationPath string `validate:"nonzero" json:"destinationPath"` -} - -// RenameFileParam represents parameter to rename file api -type RenameFileParam struct { - FilePath string `validate:"nonzero" json:"filePath"` - NewFileName string `validate:"nonzero" json:"newFileName"` - PurgeCache bool `json:"purgeCache"` -} - -// PurgeRequestId contains purge request ids -type PurgeRequestId struct { - RequestId string `json:"purgeRequestId"` -} - -// RenameFileResponse represents response struct of rename File api -type RenameFileResponse struct { - Data PurgeRequestId - api.Response -} - -// JobStatus represents response Data to job status api -type JobStatus struct { - JobId string `json:"jobId"` - Type string `json:"type"` - Status string `json:"status"` -} - -// JobStatusResponse represents response to job status api -type JobStatusResponse struct { - Data JobStatus - api.Response -} - -type ErrorMissingFileIds struct { - Message string `json:"message"` - MissingFileIds []string `json:"missingFileIds"` - err error -} - -func (e *ErrorMissingFileIds) Error() string { - return fmt.Sprintf("%s, %s", e.Message, strings.Join(e.MissingFileIds, ",")) -} -func (e *ErrorMissingFileIds) Unwrap() error { - return e.err -} - -type ErrorPartialSuccess struct { - UpdatedFileIds []string `json:"successfullyUpdatedFileIds"` - Errors []map[string]string `json:"errors"` -} - -func (e *ErrorPartialSuccess) Error() string { - return fmt.Sprintf("%v", e.Errors) -} - -// Files retrieves media library files. Filter options can be supplied as FilesParams. -func (m *API) Files(ctx context.Context, params FilesParam) (*FilesResponse, error) { - values, err := api.StructToParams(params) - if err != nil { - return nil, err - } - - var query = values.Encode() - - if query != "" { - query = "?" + query - } - - response := &FilesResponse{} - - resp, err := m.get(ctx, "files"+query, response) - - if err != nil { - return response, err - } - - if resp.StatusCode != 200 { - err = response.ParseError() - } else { - err = json.Unmarshal(response.Body(), &response.Data) - } - - return response, err -} - -// FileById returns details of single file by provided id -func (m *API) FileById(ctx context.Context, fileId string) (*FileResponse, error) { - response := &FileResponse{} - - resp, err := m.get(ctx, fmt.Sprintf("files/%s/details", fileId), response) - - defer api.DeferredBodyClose(resp) - - if err != nil { - return response, err - } - - if resp.StatusCode != 200 { - err = response.ParseError() - } else { - err = json.Unmarshal(response.Body(), &response.Data) - } - return response, err -} - -// FileVersions fetches given file version specified by version id or all versions if versionId not supplied -func (m *API) FileVersions(ctx context.Context, params FileVersionsParam) (*FilesResponse, error) { - parts := []string{"files", params.FileId, "versions"} - if params.VersionId != "" { - parts = append(parts, params.VersionId) - } - - if err := validator.Validate(¶ms); err != nil { - return nil, err - } - - response := &FilesResponse{} - - resp, err := m.get(ctx, strings.Join(parts, "/"), response) - - if err != nil { - return response, err - } - - if resp.StatusCode != 200 { - err = response.ParseError() - } else { - if params.VersionId == "" { - err = json.Unmarshal(response.Body(), &response.Data) - } else { - var file = File{} - if err = json.Unmarshal(response.Body(), &file); err == nil { - response.Data = []File{file} - } - } - } - return response, err -} - -// UpdateFile updates single file properties specified by UpdateFileParam -func (m *API) UpdateFile(ctx context.Context, fileId string, params UpdateFileParam) (*FileResponse, error) { - response := &FileResponse{} - var err error - - if fileId == "" { - return nil, errors.New("fileId can not be empty") - } - - resp, err := m.patch(ctx, fmt.Sprintf("files/%s/details", fileId), params, response) - - if err != nil { - return response, err - } - - if resp.StatusCode != 200 { - err = response.ParseError() - } else { - err = json.Unmarshal(response.Body(), &response.Data) - } - return response, err -} - -// AddTags assigns tags to bulk files specified by FileIds -func (m *API) AddTags(ctx context.Context, params TagsParam) (*TagsResponse, error) { - response := &TagsResponse{} - var err error - - resp, err := m.post(ctx, "files/addTags", params, response) - - if err != nil { - return response, err - } - - switch resp.StatusCode { - case 200: - err = json.Unmarshal(response.Body(), &response.Data) - break - case 404: - var errMissing = &ErrorMissingFileIds{err: api.ErrNotFound} - var err = json.Unmarshal(response.Body(), errMissing) - if err != nil { - return nil, err - } - //errMissing.err = api.ErrNotFound - return nil, errMissing - case 207: - var errPartial = &ErrorPartialSuccess{} - var err = json.Unmarshal(response.Body(), errPartial) - if err != nil { - return nil, err - } - - return nil, errPartial - default: - err = response.ParseError() - } - return response, err -} - -// RemoveTags removes tags from bulk files specified by FileIds -func (m *API) RemoveTags(ctx context.Context, params TagsParam) (*TagsResponse, error) { - response := &TagsResponse{} - var err error - - resp, err := m.post(ctx, "files/removeTags", params, response) - - if err != nil { - return response, err - } - - switch resp.StatusCode { - case 200: - err = json.Unmarshal(response.Body(), &response.Data) - break - case 404: - var errMissing = &ErrorMissingFileIds{err: api.ErrNotFound} - var err = json.Unmarshal(response.Body(), errMissing) - if err != nil { - return nil, err - } - return nil, errMissing - case 207: - var errPartial = &ErrorPartialSuccess{} - var err = json.Unmarshal(response.Body(), errPartial) - if err != nil { - return nil, err - } - - return nil, errPartial - default: - err = response.ParseError() - } - return response, err -} - -// RemoveAITags removes tags from bulk files specified by FileIds -func (m *API) RemoveAITags(ctx context.Context, params AITagsParam) (*TagsResponse, error) { - response := &TagsResponse{} - var err error - - resp, err := m.post(ctx, "files/removeAITags", params, response) - - if err != nil { - return response, err - } - - if resp.StatusCode != 200 { - err = response.ParseError() - } else { - err = json.Unmarshal(response.Body(), &response.Data) - } - - return response, err -} - -// DeleteFile removes file by FileId from media library -func (m *API) DeleteFile(ctx context.Context, fileId string) (*api.Response, error) { - var err error - response := &api.Response{} - - if fileId == "" { - return nil, errors.New("fileId can not be empty") - } - - resp, err := m.delete(ctx, "files/"+fileId, nil, response) - - if err != nil { - return response, err - } - - if resp.StatusCode != 204 { - err = response.ParseError() - } - return response, err -} - -// DeleteFileVersion removes given file version -func (m *API) DeleteFileVersion(ctx context.Context, fileId string, versionId string) (*api.Response, error) { - var err error - response := &api.Response{} - - if fileId == "" { - return nil, errors.New("fileId can not be empty") - } - - if versionId == "" { - return nil, errors.New("versionId can not be empty") - } - - resp, err := m.delete(ctx, fmt.Sprintf("files/%s/versions/%s", fileId, versionId), nil, response) - - if err != nil { - return response, err - } - - if resp.StatusCode != 204 { - err = response.ParseError() - } - return response, err -} - -// DeleteBulkFiles deletes multiple files from media library -func (m *API) DeleteBulkFiles(ctx context.Context, param FileIdsParam) (*DeleteFilesResponse, error) { - var err error - response := &DeleteFilesResponse{} - - if err = validator.Validate(¶m); err != nil { - return nil, err - } - - resp, err := m.post(ctx, "files/batch/deleteByFileIds", param, response) - - if err != nil { - return response, err - } - - if resp.StatusCode == 200 || resp.StatusCode == 207 { - err = json.Unmarshal(response.Body(), &response.Data) - } else { - err = response.ParseError() - } - return response, err -} - -// CopyFile copies a file to target path -func (m *API) CopyFile(ctx context.Context, param CopyFileParam) (*api.Response, error) { - var err error - - response := &api.Response{} - - if err = validator.Validate(¶m); err != nil { - return nil, err - } - - resp, err := m.post(ctx, "files/copy", ¶m, response) - - if err != nil { - return response, err - } - - if resp.StatusCode != 204 { - err = response.ParseError() - } - return response, err -} - -// MoveFile moves a file to target path -func (m *API) MoveFile(ctx context.Context, param MoveFileParam) (*api.Response, error) { - var err error - - response := &api.Response{} - - if err = validator.Validate(¶m); err != nil { - return nil, err - } - - resp, err := m.post(ctx, "files/move", ¶m, response) - - if err != nil { - return response, err - } - - if resp.StatusCode != 204 { - err = response.ParseError() - } - return response, err -} - -// RenameFile renames a file to new name as specified in RenameFileParam struct and optionally includes purge request id -func (m *API) RenameFile(ctx context.Context, param RenameFileParam) (*RenameFileResponse, error) { - var err error - var response = &RenameFileResponse{} - - if err = validator.Validate(¶m); err != nil { - return nil, err - } - - resp, err := m.put(ctx, "files/rename", ¶m, response) - defer api.DeferredBodyClose(resp) - - if err != nil { - return response, err - } - - if resp.StatusCode != 200 { - err = response.ParseError() - } else { - err = json.Unmarshal(response.Body(), &response.Data) - } - - return response, err -} - -// RestoreVersion sets specified verison of the file as current version -func (m *API) RestoreVersion(ctx context.Context, param FileVersionsParam) (*FileResponse, error) { - var err error - var response = &FileResponse{} - - if err = validator.Validate(¶m); err != nil { - return nil, err - } - - resp, err := m.delete(ctx, fmt.Sprintf("files/%s/versions/%s/restore", - param.FileId, param.VersionId), nil, response) - - if resp.StatusCode != 200 { - err = response.ParseError() - } else { - err = json.Unmarshal(response.Body(), &response.Data) - } - return response, err -} - -func (m *API) BulkJobStatus(ctx context.Context, jobId string) (*JobStatusResponse, error) { - var err error - var response = &JobStatusResponse{} - - if jobId == "" { - return nil, errors.New("jobId can not be blank") - } - - resp, err := m.get(ctx, "bulkJobs/"+jobId, response) - - if err != nil { - return response, err - } - - if resp.StatusCode != 200 { - err = response.ParseError() - } else { - err = json.Unmarshal(response.Body(), &response.Data) - } - return response, err -} diff --git a/api/media/assets_test.go b/api/media/assets_test.go deleted file mode 100644 index e016c9d..0000000 --- a/api/media/assets_test.go +++ /dev/null @@ -1,932 +0,0 @@ -package media - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "log" - "net/http/httptest" - "os" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/imagekit-developer/imagekit-go/api/extension" - iktest "github.com/imagekit-developer/imagekit-go/test" -) - -var ctx = context.Background() - -var respBody = `[{"fileId":"6283b04dc82abf6294aee010","name":"beauty_of_nature_12_6S7aNLP3-.jpg","filePath":"/beauty_of_nature_12_6S7aNLP3-.jpg","Tags":null,"AITags":null,"versionInfo":{"id":"6283b04dc82abf6294aee010","name":"Version 2"},"isPrivateFile":false,"customCoordinates":null,"url":"https://ik.imagekit.io/dk1m7xkgi/beauty_of_nature_12_6S7aNLP3-.jpg","thumbnail":"https://ik.imagekit.io/dk1m7xkgi/tr:n-ik_ml_thumbnail/beauty_of_nature_12_6S7aNLP3-.jpg","fileType":"image","mime":"image/png","height":133,"Width":200,"size":26509,"hasAlpha":true,"customMetadata":{"price":10},"embeddedMetadata":{"DateCreated":"2022-06-07T15:20:32.104Z","DateTimeCreated":"2022-06-07T15:20:32.105Z","ImageHeight":133,"ImageWidth":200},"createdAt":"2022-05-17T14:25:17.543Z","updatedAt":"2022-06-07T15:20:32.107Z"}]` - -var singleFileResp string -var missingFileIdsBody = ` - { - "message": "The requested file(s) does not exist.", - "missingFileIds": [ - "yyy" - ] - }` - -var partialSuccessBody = ` - { - "successfullyUpdatedFileIds": [ - "xxx" - ], - "errors": [ - { - "fileId": "yyy", - "error": "Error in removing tags" - } - ] - }` -var assetsArr []File -var asset File -var mediaApi *API -var testExtenstions = []extension.IExtension{ - &extension.AutoTag{ - Name: extension.GoogleAutoTag, - MinConfidence: 50, - MaxTags: 10, - }, - extension.NewRemoveBg(extension.RemoveBgOption{ - AddShadow: true, - SemiTransparency: true, - BgColor: "#000000", - BgImageUrl: "http://test/test.jpg", - }), -} -var customMetadata = map[string]any{ - "brand": "nike", - "size": 10, -} - -func TestMain(m *testing.M) { - var err error - mediaApi, err = NewFromConfiguration(iktest.Cfg) - - if err != nil { - log.Fatal(err) - } - - if err = json.Unmarshal([]byte(respBody), &assetsArr); err != nil { - log.Fatal(err) - } - - singleFileResp = respBody[1 : len(respBody)-1] - err = json.Unmarshal([]byte(singleFileResp), &asset) - if err != nil { - log.Fatal(err) - } - - os.Exit(m.Run()) -} - -func Test_New(t *testing.T) { - os.Setenv("IMAGEKIT_PRIVATE_KEY", "private_") - os.Setenv("IMAGEKIT_PUBLIC_KEY", "public_") - os.Setenv("IMAGEKIT_ENDPOINT_URL", "https://ik.imagekit.io/test/") - - defer os.Unsetenv("IMAGEKIT_PRIVATE_KEY") - defer os.Unsetenv("IMAGEKIT_PUBLIC_KEY") - defer os.Unsetenv("IMAGEKIT_ENDPOINT_URL") - - var api any - api, err := New() - - if err != nil { - t.Fatal(err) - } - - if api == nil { - t.Error("New() returned null") - } - - if _, ok := api.(*API); !ok { - t.Error("New() did not return *API") - } -} - -func TestMedia_Files(t *testing.T) { - var expected = assetsArr - var cases = map[string]struct { - params FilesParam - result string - }{ - "default": { - params: FilesParam{}, - result: "/files", - }, - "with-params": { - params: FilesParam{ - Type: ListFile, - Sort: AscName, - Path: "/test", - SearchQuery: `createdAt > "7d" AND name: "file-name"`, - FileType: Image, - Tags: "tag1,tag2", - Limit: 100, - Skip: 10, - }, - result: "/files?fileType=image&limit=100&path=%2Ftest&searchQuery=createdAt+%3E+%227d%22+AND+name%3A+%22file-name%22&skip=10&sort=ASC_NAME&tags=tag1%2Ctag2&type=file", - }, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - httpTest := iktest.NewHttp(t) - - ts := httptest.NewServer(httpTest.Handler(200, string(respBody))) - defer ts.Close() - - mediaApi.Config.API.Prefix = ts.URL + "/" - - resp, err := mediaApi.Files(ctx, tc.params) - - if err != nil { - t.Error(err) - } - - if !cmp.Equal(resp.Data, expected) { - t.Errorf("\n%v\n%v\n", resp.Data, expected) - } - - httpTest.Test(tc.result, "GET", nil) - }) - } - - errServer := iktest.NewErrorServer(t) - mediaApi.Config.API.Prefix = errServer.Url() + "/" - errServer.TestErrors(func() error { - _, err := mediaApi.Files(ctx, FilesParam{}) - return err - }) -} - -func TestMedia_FileById(t *testing.T) { - var expected = asset - var mockBody = respBody[1 : len(respBody)-1] - - var cases = map[string]struct { - fileId string - url string - result File - body string - statusCode int - shouldFail bool - }{ - "get asset successfully": { - fileId: "123", - url: "/files/123/details", - body: mockBody, - result: expected, - statusCode: 200, - shouldFail: false, - }, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - httpTest := iktest.NewHttp(t) - - ts := httptest.NewServer(httpTest.Handler(tc.statusCode, tc.body)) - defer ts.Close() - - mediaApi.Config.API.Prefix = ts.URL + "/" - - resp, err := mediaApi.FileById(ctx, tc.fileId) - - if tc.shouldFail && err == nil { - t.Error("expected error") - } - - if !tc.shouldFail && err != nil { - t.Error(err) - } - - if !cmp.Equal(resp.Data, tc.result) { - t.Errorf("\n%v\n%v\n", resp.Data, expected) - } - - httpTest.Test(tc.url, "GET", nil) - }) - } - errServer := iktest.NewErrorServer(t) - mediaApi.Config.API.Prefix = errServer.Url() + "/" - - errServer.TestErrors(func() error { - _, err := mediaApi.FileById(ctx, "111") - return err - }) -} - -func TestMedia_FileVersions(t *testing.T) { - var cases = map[string]struct { - fileId string - versionId string - body string - statusCode int - shouldFail bool - }{ - "all versions": { - fileId: "6283b04dc82abf6294aee010", - versionId: "v123", - body: singleFileResp, - statusCode: 200, - shouldFail: false, - }, - "invalid": { - fileId: "", - versionId: "", - body: singleFileResp, - statusCode: 200, - shouldFail: true, - }, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - var expectedUrl = "/files/" + tc.fileId + "/versions" - - if tc.versionId != "" { - expectedUrl = expectedUrl + "/" + tc.versionId - } - - httpTest := iktest.NewHttp(t) - - ts := httptest.NewServer(httpTest.Handler(tc.statusCode, string(tc.body))) - defer ts.Close() - - mediaApi.Config.API.Prefix = ts.URL + "/" - - params := FileVersionsParam{ - FileId: tc.fileId, - VersionId: tc.versionId, - } - _, err := mediaApi.FileVersions(ctx, params) - - if tc.shouldFail && err == nil { - t.Error("expected error") - } - - if !tc.shouldFail && err != nil { - t.Error(err) - } - - if !tc.shouldFail { - httpTest.Test(expectedUrl, "GET", nil) - } - }) - } - errServer := iktest.NewErrorServer(t) - mediaApi.Config.API.Prefix = errServer.Url() + "/" - - errServer.TestErrors(func() error { - _, err := mediaApi.FileVersions(ctx, FileVersionsParam{FileId: "111", VersionId: "v1"}) - return err - }) -} - -func TestMedia_UpdateFile(t *testing.T) { - var expected = asset - var mockBody = respBody[1 : len(respBody)-1] - - var cases = map[string]struct { - result *File - fileId string - body string - params UpdateFileParam - statusCode int - shouldFail bool - }{ - "update asset": { - result: &expected, - fileId: "file_id", - body: mockBody, - statusCode: 200, - shouldFail: false, - params: UpdateFileParam{ - RemoveAITags: []string{"one", "two"}, - WebhookUrl: "http://example.com/hook", - Tags: []string{"abc", "def"}, - CustomCoordinates: "12,11,22,22", - Extensions: testExtenstions, - CustomMetadata: customMetadata, - }, - }, - "missing-file-id": { - result: &expected, - fileId: "", - body: mockBody, - statusCode: 200, - shouldFail: true, - params: UpdateFileParam{ - RemoveAITags: []string{"one", "two"}, - WebhookUrl: "http://example.com/hook", - Tags: []string{"abc", "def"}, - CustomCoordinates: "12,11,22,22", - Extensions: testExtenstions, - CustomMetadata: customMetadata, - }, - }, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - httpTest := iktest.NewHttp(t) - ts := httptest.NewServer(httpTest.Handler(200, string(tc.body))) - defer ts.Close() - - mediaApi.Config.API.Prefix = ts.URL + "/" - - response, err := mediaApi.UpdateFile(ctx, tc.fileId, tc.params) - - var expectedUrl = "/files/" + tc.fileId + "/details" - - if tc.shouldFail == false { - httpTest.Test(expectedUrl, "PATCH", tc.params) - } - - if tc.shouldFail == true && err == nil { - t.Error("expected err") - } - - if tc.shouldFail == false && err != nil { - t.Error("err not nil" + err.Error()) - } - - if !tc.shouldFail && !cmp.Equal(tc.result, &response.Data) { - t.Errorf("unexpected response %v\n%v", tc.result, response.Data) - } - - }) - } - errServer := iktest.NewErrorServer(t) - mediaApi.Config.API.Prefix = errServer.Url() + "/" - - errServer.TestErrors(func() error { - _, err := mediaApi.UpdateFile(ctx, "111", UpdateFileParam{}) - return err - }) -} - -func TestMedia_AddTags(t *testing.T) { - var ids = []string{"xxx", "yyy"} - var tags = []string{"tag1", "tag2"} - var resp = UpdatedIds{ - FileIds: ids, - } - - respBody, _ := json.Marshal(&resp) - - params := TagsParam{ - FileIds: ids, - Tags: tags, - } - - cases := map[string]struct { - params TagsParam - body string - statusCode int - }{ - "add tags": { - body: string(respBody), - statusCode: 200, - }, - "partial success": { - body: partialSuccessBody, - statusCode: 207, - }, - "file id not found": { - body: missingFileIdsBody, - statusCode: 404, - }, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - httpTest := iktest.NewHttp(t) - - ts := httptest.NewServer(httpTest.Handler(tc.statusCode, tc.body)) - defer ts.Close() - - mediaApi.Config.API.Prefix = ts.URL + "/" - - response, err := mediaApi.AddTags(ctx, params) - - var url = "/files/addTags" - - switch tc.statusCode { - case 200: - if err != nil { - t.Error("unexpected error", err) - } else { - httpTest.Test(url, "POST", params) - if !cmp.Equal(response.Data, resp) { - t.Errorf("%v\n%v", response.Data, resp) - } - } - break - case 207: - var errPartial *ErrorPartialSuccess - if !errors.As(err, &errPartial) { - log.Println(err) - t.Error("error is not type ErrorPartialSuccess") - } - break - case 404: - var e *ErrorMissingFileIds - if !errors.As(err, &e) { - log.Println(e) - t.Error("error is not type ErrorMissingFileIds") - } - break - } - }) - } - - errServer := iktest.NewErrorServer(t) - mediaApi.Config.API.Prefix = errServer.Url() + "/" - errServer.TestErrors(func() error { - _, err := mediaApi.AddTags(ctx, TagsParam{}) - return err - }) -} - -func TestMedia_RemoveTags(t *testing.T) { - var ids = []string{"xxx", "yyy"} - var tags = []string{"tag1", "tag2"} - var resp = UpdatedIds{ - FileIds: ids, - } - params := TagsParam{ - FileIds: ids, - Tags: tags, - } - - respBody, _ := json.Marshal(&resp) - - var cases = map[string]struct { - params TagsParam - body string - statusCode int - }{ - "remove tags": { - body: string(respBody), - statusCode: 200, - }, - "partial success": { - body: partialSuccessBody, - statusCode: 207, - }, - "missing file ids": { - body: missingFileIdsBody, - statusCode: 404, - }, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - httpTest := iktest.NewHttp(t) - - ts := httptest.NewServer(httpTest.Handler(tc.statusCode, tc.body)) - defer ts.Close() - - mediaApi.Config.API.Prefix = ts.URL + "/" - - response, err := mediaApi.RemoveTags(ctx, params) - var url = "/files/removeTags" - switch tc.statusCode { - case 200: - if err != nil { - t.Error("unexpected error", err) - } else { - httpTest.Test(url, "POST", params) - if !cmp.Equal(response.Data, resp) { - t.Errorf("%v\n%v", response.Data, resp) - } - } - break - case 207: - var errPartial *ErrorPartialSuccess - if !errors.As(err, &errPartial) { - log.Println(err) - t.Error("error is not type ErrorPartialSuccess") - } - break - case 404: - var e *ErrorMissingFileIds - if !errors.As(err, &e) { - log.Println(e) - t.Error("error is not type ErrorMissingFileIds") - } - break - } - }) - } - errServer := iktest.NewErrorServer(t) - mediaApi.Config.API.Prefix = errServer.Url() + "/" - - errServer.TestErrors(func() error { - _, err := mediaApi.RemoveTags(ctx, params) - return err - }) -} - -func TestMedia_RemoveAITags(t *testing.T) { - var ids = []string{"xxx", "yyy"} - var tags = []string{"tag1", "tag2"} - var resp = UpdatedIds{ - FileIds: ids, - } - - respBody, _ := json.Marshal(&resp) - params := AITagsParam{ - FileIds: ids, - AITags: tags, - } - - var cases = map[string]struct { - body string - statusCode int - }{ - "remove tags": { - body: string(respBody), - statusCode: 200, - }, - "partial success": { - body: partialSuccessBody, - statusCode: 207, - }, - "missing file ids": { - body: missingFileIdsBody, - statusCode: 404, - }, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - httpTest := iktest.NewHttp(t) - - ts := httptest.NewServer(httpTest.Handler(tc.statusCode, tc.body)) - defer ts.Close() - - mediaApi.Config.API.Prefix = ts.URL + "/" - - response, err := mediaApi.RemoveAITags(ctx, params) - - if tc.statusCode == 200 { - if err != nil { - t.Error(err) - } else if !cmp.Equal(response.Data, resp) { - t.Errorf("%v\n%v", response.Data, resp) - } - } - - httpTest.Test("/files/removeAITags", "POST", params) - }) - } - errServer := iktest.NewErrorServer(t) - mediaApi.Config.API.Prefix = errServer.Url() + "/" - - errServer.TestErrors(func() error { - _, err := mediaApi.RemoveAITags(ctx, params) - return err - }) -} - -func TestMedia_DeleteFile(t *testing.T) { - var err error - - httpTest := iktest.NewHttp(t) - - ts := httptest.NewServer(httpTest.Handler(200, "1")) - defer ts.Close() - - mediaApi.Config.API.Prefix = ts.URL + "/" - _, err = mediaApi.DeleteFile(ctx, "file_id") - - if err != nil { - t.Error(err) - } - - httpTest.Test("/files/file_id", "DELETE", nil) - - _, err = mediaApi.DeleteFile(ctx, "") - - if err == nil { - t.Error("expected error") - } - - errServer := iktest.NewErrorServer(t) - mediaApi.Config.API.Prefix = errServer.Url() + "/" - - errServer.TestErrors(func() error { - _, err = mediaApi.DeleteFile(ctx, "file_id") - return err - }) -} - -func TestMedia_DeleteFileVersion(t *testing.T) { - var err error - - httpTest := iktest.NewHttp(t) - - ts := httptest.NewServer(httpTest.Handler(204, "1")) - defer ts.Close() - - mediaApi.Config.API.Prefix = ts.URL + "/" - _, err = mediaApi.DeleteFileVersion(ctx, "file_id", "v2") - - if err != nil { - t.Error(err) - } - - url := "/files/file_id/versions/v2" - - httpTest.Test(url, "DELETE", nil) - - _, err = mediaApi.DeleteFileVersion(ctx, "", "v2") - - if err == nil { - t.Error("expected error") - } - - _, err = mediaApi.DeleteFileVersion(ctx, "file_id", "") - - if err == nil { - t.Error("expected error") - } - - errServer := iktest.NewErrorServer(t) - mediaApi.Config.API.Prefix = errServer.Url() + "/" - - errServer.TestErrors(func() error { - _, err = mediaApi.DeleteFileVersion(ctx, "file_id", "v2") - return err - }) -} - -func TestMedia_DeleteBulkFiles(t *testing.T) { - var err error - var param = FileIdsParam{ - FileIds: []string{ - "file_id1", "file_id2", - }, - } - var respBody = `{"successfullyDeletedFileIds":["file_id1","file_id2"]}` - - httpTest := iktest.NewHttp(t) - - ts := httptest.NewServer(httpTest.Handler(200, string(respBody))) - defer ts.Close() - - mediaApi.Config.API.Prefix = ts.URL + "/" - - resp, err := mediaApi.DeleteBulkFiles(ctx, param) - - if err != nil { - t.Error(err) - } - - log.Println("delete bulk assets test: ", resp.Data.Errors) - - if !cmp.Equal(resp.Data.FileIds, param.FileIds) { - t.Errorf("expected: %v, got: %v", param.FileIds, resp.Data.FileIds) - } - httpTest.Test("/files/batch/deleteByFileIds", "POST", param) - - resp, err = mediaApi.DeleteBulkFiles(ctx, FileIdsParam{}) - - if err == nil { - t.Error("expected error") - } - - errServer := iktest.NewErrorServer(t) - mediaApi.Config.API.Prefix = errServer.Url() + "/" - - errServer.TestErrors(func() error { - _, err := mediaApi.DeleteBulkFiles(ctx, param) - return err - }) -} - -func TestMedia_CopyFile(t *testing.T) { - var err error - var param = CopyFileParam{ - SourcePath: "/file.jpg", - DestinationPath: "/natural/file.jpg", - IncludeFileVersions: true, - } - - httpTest := iktest.NewHttp(t) - - ts := httptest.NewServer(httpTest.Handler(204, "")) - defer ts.Close() - - mediaApi.Config.API.Prefix = ts.URL + "/" - - _, err = mediaApi.CopyFile(ctx, param) - if err != nil { - t.Error(err) - } - httpTest.Test("/files/copy", "POST", param) - - _, err = mediaApi.CopyFile(ctx, CopyFileParam{}) - - if err == nil { - t.Error(err) - } - errServer := iktest.NewErrorServer(t) - mediaApi.Config.API.Prefix = errServer.Url() + "/" - - errServer.TestErrors(func() error { - _, err = mediaApi.CopyFile(ctx, param) - return err - }) -} - -func TestMedia_MoveFile(t *testing.T) { - var err error - var param = MoveFileParam{ - SourcePath: "/file.jpg", - DestinationPath: "/natural/", - } - - httpTest := iktest.NewHttp(t) - - ts := httptest.NewServer(httpTest.Handler(204, "")) - defer ts.Close() - - mediaApi.Config.API.Prefix = ts.URL + "/" - - _, err = mediaApi.MoveFile(ctx, param) - if err != nil { - t.Error(err) - } - - httpTest.Test("/files/move", "POST", param) - - _, err = mediaApi.MoveFile(ctx, MoveFileParam{}) - if err == nil { - t.Error("expected error") - } - - errServer := iktest.NewErrorServer(t) - mediaApi.Config.API.Prefix = errServer.Url() + "/" - - errServer.TestErrors(func() error { - _, err = mediaApi.MoveFile(ctx, param) - return err - }) -} - -func TestMedia_RenameFile(t *testing.T) { - var cases = map[string]struct { - param RenameFileParam - body string - statusCode int - }{ - "rename asset": { - param: RenameFileParam{ - FilePath: "/some/file.jpg", - NewFileName: "/default.jpg", - PurgeCache: true, - }, - body: `{"purgeRequestId":"123"}`, - statusCode: 200, - }, - "without purge": { - param: RenameFileParam{ - FilePath: "/some/file.jpg", - NewFileName: "/default.jpg", - }, - body: `{}`, - statusCode: 200, - }, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - httpTest := iktest.NewHttp(t) - - ts := httptest.NewServer(httpTest.Handler(tc.statusCode, tc.body)) - defer ts.Close() - - mediaApi.Config.API.Prefix = ts.URL + "/" - - resp, err := mediaApi.RenameFile(ctx, tc.param) - if err != nil { - t.Error(err) - } - - if tc.param.PurgeCache == true && resp.Data.RequestId != "123" { - t.Error("unexpected request id returned") - } - - httpTest.Test("/files/rename", "PUT", tc.param) - }) - } - httpTest := iktest.NewHttp(t) - ts := httptest.NewServer(httpTest.Handler(200, "")) - defer ts.Close() - - mediaApi.Config.API.Prefix = ts.URL + "/" - - _, err := mediaApi.RenameFile(ctx, RenameFileParam{}) - if err == nil { - t.Error(err) - } - errServer := iktest.NewErrorServer(t) - mediaApi.Config.API.Prefix = errServer.Url() + "/" - - errServer.TestErrors(func() error { - _, err := mediaApi.RenameFile(ctx, RenameFileParam{ - FilePath: "/some/file.jpg", - NewFileName: "/default.jpg", - }) - return err - }) -} -func TestMedia_RestoreVersion(t *testing.T) { - var err error - - var param = FileVersionsParam{ - FileId: "file_id", - VersionId: "v1", - } - - httpTest := iktest.NewHttp(t) - - ts := httptest.NewServer(httpTest.Handler(200, singleFileResp)) - defer ts.Close() - - mediaApi.Config.API.Prefix = ts.URL + "/" - - resp, err := mediaApi.RestoreVersion(ctx, param) - if err != nil { - t.Error(err) - } - - if !cmp.Equal(resp.Data, asset) { - t.Error("unexpected response") - } - - expectedUrl := fmt.Sprintf("/files/%s/versions/%s/restore", - param.FileId, param.VersionId) - - httpTest.Test(expectedUrl, "DELETE", param) - - resp, err = mediaApi.RestoreVersion(ctx, FileVersionsParam{}) - if err == nil { - t.Error("expected error") - } - - errServer := iktest.NewErrorServer(t) - mediaApi.Config.API.Prefix = errServer.Url() + "/" - - errServer.TestErrors(func() error { - _, err := mediaApi.RestoreVersion(ctx, param) - return err - }) -} - -func TestMedia_BulkJobStatus(t *testing.T) { - var err error - var mockBody = `{"jobId":"job_id","type":"MOVE_FOLDER","status":"Completed"}` - var res = JobStatusResponse{ - Data: JobStatus{"job_id", "MOVE_FOLDER", "Completed"}, - } - _ = json.Unmarshal([]byte(mockBody), &res) - var jobId = "job_id" - httpTest := iktest.NewHttp(t) - - ts := httptest.NewServer(httpTest.Handler(200, mockBody)) - defer ts.Close() - - mediaApi.Config.API.Prefix = ts.URL + "/" - - resp, err := mediaApi.BulkJobStatus(ctx, jobId) - if err != nil { - t.Error(err) - } - - if !cmp.Equal(resp.Data, res.Data) { - t.Error("unexpected response") - } - - httpTest.Test("/bulkJobs/"+jobId, "GET", nil) - - resp, err = mediaApi.BulkJobStatus(ctx, "") - if err == nil { - t.Error("expected error") - } - - errServer := iktest.NewErrorServer(t) - mediaApi.Config.API.Prefix = errServer.Url() + "/" - - errServer.TestErrors(func() error { - _, err := mediaApi.BulkJobStatus(ctx, jobId) - return err - }) -} diff --git a/api/media/cache.go b/api/media/cache.go deleted file mode 100644 index 60380ee..0000000 --- a/api/media/cache.go +++ /dev/null @@ -1,79 +0,0 @@ -package media - -import ( - "context" - "encoding/json" - "errors" - - "github.com/imagekit-developer/imagekit-go/api" - "gopkg.in/validator.v2" -) - -type RequestId struct { - RequestId string `json:"requestId"` -} - -type PurgeCacheResponse struct { - Data RequestId - api.Response -} - -type PurgeCacheParam struct { - Url string `validate:"nonzero" json:"url"` -} - -type PurgeCacheStatus struct { - Status string `json:"status"` -} - -type PurgeCacheStatusResponse struct { - Data PurgeCacheStatus - api.Response -} - -// PurgeCache purges cache and returns requestId in response data. -func (m *API) PurgeCache(ctx context.Context, param PurgeCacheParam) (*PurgeCacheResponse, error) { - var err error - var response = &PurgeCacheResponse{} - - if err = validator.Validate(¶m); err != nil { - return nil, err - } - - resp, err := m.post(ctx, "files/purge", ¶m, response) - - if err != nil { - return response, err - } - - if resp.StatusCode < 200 || resp.StatusCode > 299 { - err = response.ParseError() - } else { - err = json.Unmarshal(response.Body(), &response.Data) - } - return response, err -} - -// PurgeCacheStatus returns status of purge cache request -func (m *API) PurgeCacheStatus(ctx context.Context, requestId string) (*PurgeCacheStatusResponse, error) { - var err error - var response = &PurgeCacheStatusResponse{} - - if requestId == "" { - return nil, errors.New("requestId can not be empty") - } - - resp, err := m.get(ctx, "files/purge/"+requestId, response) - - if err != nil { - return response, err - } - - if resp.StatusCode != 200 { - err = response.ParseError() - } else { - err = json.Unmarshal(response.Body(), &response.Data) - } - return response, err - -} diff --git a/api/media/cache_test.go b/api/media/cache_test.go deleted file mode 100644 index 9cbab01..0000000 --- a/api/media/cache_test.go +++ /dev/null @@ -1,122 +0,0 @@ -package media - -import ( - "encoding/json" - "net/http/httptest" - "testing" - - "github.com/google/go-cmp/cmp" - iktest "github.com/imagekit-developer/imagekit-go/test" -) - -func TestMedia_PurgeCache(t *testing.T) { - var param = PurgeCacheParam{ - Url: "https://ik.imagekit.io/dk1m7xkgi/200046731_6088567407851959_2178338512936890186_n_ygW1IEmmc1.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1654873963613", - } - - rs := PurgeCacheResponse{ - Data: RequestId{ - RequestId: "xxx", - }, - } - respBody, _ := json.Marshal(&rs.Data) - - var cases = map[string]struct { - param PurgeCacheParam - result string - expectErr bool - }{ - "purge-cache": { - param, - string(respBody), - false, - }, - "empty-url": { - PurgeCacheParam{}, - "{}", - true, - }, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - httpTest := iktest.NewHttp(t) - - ts := httptest.NewServer(httpTest.Handler(201, tc.result)) - defer ts.Close() - - mediaApi.Config.API.Prefix = ts.URL + "/" - response, err := mediaApi.PurgeCache(ctx, param) - - if err != nil { - t.Error(err) - } - - response, err = mediaApi.PurgeCache(ctx, tc.param) - - if tc.expectErr && err == nil { - t.Error("expected error") - } - - if !tc.expectErr && err != nil { - t.Error("Unexpected error") - } - - if !tc.expectErr && !cmp.Equal(response.Data.RequestId, "xxx") { - t.Error(response.Data) - } - - if name != "empty-url" { - httpTest.Test("/files/purge", "POST", tc.param) - } - }) - } - - errServer := iktest.NewErrorServer(t) - mediaApi.Config.API.Prefix = errServer.Url() + "/" - - errServer.TestErrors(func() error { - _, err := mediaApi.PurgeCache(ctx, param) - return err - }) -} - -func TestMedia_PurgeCacheStatus(t *testing.T) { - reqId := "62a4b" - var rs = PurgeCacheStatusResponse{ - Data: PurgeCacheStatus{ - Status: "Pending", - }, - } - - respBody, _ := json.Marshal(&rs.Data) - - httpTest := iktest.NewHttp(t) - - ts := httptest.NewServer(httpTest.Handler(200, string(respBody))) - defer ts.Close() - - mediaApi.Config.API.Prefix = ts.URL + "/" - response, err := mediaApi.PurgeCacheStatus(ctx, reqId) - - if err != nil { - t.Error(err) - } - if !cmp.Equal(response.Data, rs.Data) { - t.Error(response.Data) - } - httpTest.Test("/files/purge/"+reqId, "GET", nil) - - response, err = mediaApi.PurgeCacheStatus(ctx, "") - if err == nil { - t.Error("expected error") - } - - errServer := iktest.NewErrorServer(t) - mediaApi.Config.API.Prefix = errServer.Url() + "/" - - errServer.TestErrors(func() error { - _, err := mediaApi.PurgeCacheStatus(ctx, reqId) - return err - }) -} diff --git a/api/media/folder.go b/api/media/folder.go deleted file mode 100644 index 9985919..0000000 --- a/api/media/folder.go +++ /dev/null @@ -1,131 +0,0 @@ -package media - -import ( - "context" - "encoding/json" - - "github.com/imagekit-developer/imagekit-go/api" - "gopkg.in/validator.v2" -) - -// CreateFolderParam represents parameter to create folder api -type CreateFolderParam struct { - FolderName string `validate:"nonzero" json:"folderName"` - ParentFolderPath string `validate:"nonzero" json:"parentFolderPath"` -} - -// DeleteFolderParam represents parameter to delete folder api -type DeleteFolderParam struct { - FolderPath string `validate:"nonzero" json:"folderPath"` -} - -// CopyFolderParam represents parameter to copy folder api -type CopyFolderParam struct { - SourceFolderPath string `validate:"nonzero" json:"sourceFolderPath"` - DestinationPath string `validate:"nonzero" json:"destinationPath"` - IncludeFileVersions bool `json:"IncludeFileVersions"` -} - -// MoveFolderParam represents parameter to move folder api -type MoveFolderParam struct { - SourceFolderPath string `validate:"nonzero" json:"sourceFolderPath"` - DestinationPath string `validate:"nonzero" json:"destinationPath"` -} - -// JobIdResponse respresents response struct with JobId for folder operations -type JobIdResponse struct { - JobId string `json:"jobId"` -} - -//FolderResponse respresents struct for response to move folder api. -type FolderResponse struct { - Data JobIdResponse - api.Response -} - -// CreateFolder creates a new folder in media library -func (m *API) CreateFolder(ctx context.Context, param CreateFolderParam) (*api.Response, error) { - var err error - var response = &api.Response{} - - if err = validator.Validate(¶m); err != nil { - return nil, err - } - - resp, err := m.post(ctx, "folder", ¶m, response) - - if err != nil { - return response, err - } - - if resp.StatusCode != 201 { - err = response.ParseError() - } - - return response, err -} - -// DeleteFolder removes the folder from media library -func (m *API) DeleteFolder(ctx context.Context, param DeleteFolderParam) (*api.Response, error) { - var err error - var response = &api.Response{} - - if err = validator.Validate(¶m); err != nil { - return nil, err - } - - resp, err := m.delete(ctx, "folder", ¶m, response) - - if err != nil { - return response, err - } - if resp.StatusCode != 204 { - err = response.ParseError() - } - return response, err -} - -// MoveFolder moves given folder to new aath in media library -func (m *API) MoveFolder(ctx context.Context, param MoveFolderParam) (*FolderResponse, error) { - var err error - var response = &FolderResponse{} - - if err = validator.Validate(¶m); err != nil { - return nil, err - } - - resp, err := m.post(ctx, "bulkJobs/moveFolder", ¶m, response) - - if err != nil { - return response, err - } - - if resp.StatusCode != 200 { - err = response.ParseError() - } else { - err = json.Unmarshal(response.Body(), &response.Data) - } - return response, err -} - -func (m *API) CopyFolder(ctx context.Context, param CopyFolderParam) (*FolderResponse, error) { - var err error - var response = &FolderResponse{} - - if err = validator.Validate(¶m); err != nil { - return nil, err - } - - resp, err := m.post(ctx, "bulkJobs/copyFolder", ¶m, response) - - if err != nil { - return response, err - } - - if resp.StatusCode != 200 { - err = response.ParseError() - } else { - err = json.Unmarshal(response.Body(), &response.Data) - } - return response, err -} diff --git a/api/media/folder_test.go b/api/media/folder_test.go deleted file mode 100644 index 06e1de1..0000000 --- a/api/media/folder_test.go +++ /dev/null @@ -1,167 +0,0 @@ -package media - -import ( - "encoding/json" - "net/http/httptest" - "testing" - - "github.com/google/go-cmp/cmp" - iktest "github.com/imagekit-developer/imagekit-go/test" -) - -func TestMedia_CreateFolder(t *testing.T) { - var err error - - var param = CreateFolderParam{ - FolderName: "testing", - ParentFolderPath: "/", - } - - httpTest := iktest.NewHttp(t) - - ts := httptest.NewServer(httpTest.Handler(201, "{}")) - defer ts.Close() - - mediaApi.Config.API.Prefix = ts.URL + "/" - - _, err = mediaApi.CreateFolder(ctx, param) - - if err != nil { - t.Error(err) - } - httpTest.Test("/folder", "POST", param) - - _, err = mediaApi.CreateFolder(ctx, CreateFolderParam{}) - - if err == nil { - t.Error("expected error") - } - - errServer := iktest.NewErrorServer(t) - mediaApi.Config.API.Prefix = errServer.Url() + "/" - - errServer.TestErrors(func() error { - _, err = mediaApi.CreateFolder(ctx, param) - return err - }) -} - -func TestMedia_DeleteFolder(t *testing.T) { - var err error - - var param = DeleteFolderParam{ - FolderPath: "testing", - } - - httpTest := iktest.NewHttp(t) - - ts := httptest.NewServer(httpTest.Handler(204, "{}")) - defer ts.Close() - - mediaApi.Config.API.Prefix = ts.URL + "/" - _, err = mediaApi.DeleteFolder(ctx, param) - - if err != nil { - t.Error(err) - } - - httpTest.Test("/folder", "DELETE", nil) - - _, err = mediaApi.DeleteFolder(ctx, DeleteFolderParam{}) - - if err == nil { - t.Error("expected error") - } - - errServer := iktest.NewErrorServer(t) - mediaApi.Config.API.Prefix = errServer.Url() + "/" - - errServer.TestErrors(func() error { - _, err = mediaApi.DeleteFolder(ctx, param) - return err - }) -} - -func TestMedia_MoveFolder(t *testing.T) { - var err error - - var param = MoveFolderParam{ - SourceFolderPath: "/src", - DestinationPath: "dest", - } - - httpTest := iktest.NewHttp(t) - - ts := httptest.NewServer(httpTest.Handler(200, `{"jobId":"xxx"}`)) - defer ts.Close() - - mediaApi.Config.API.Prefix = ts.URL + "/" - response, err := mediaApi.MoveFolder(ctx, param) - - if err != nil { - t.Error(err) - } - if !cmp.Equal(response.Data.JobId, "xxx") { - t.Error(response.Data) - } - httpTest.Test("/bulkJobs/moveFolder", "POST", param) - - response, err = mediaApi.MoveFolder(ctx, MoveFolderParam{}) - - if err == nil { - t.Error("expected error") - } - - errServer := iktest.NewErrorServer(t) - mediaApi.Config.API.Prefix = errServer.Url() + "/" - - errServer.TestErrors(func() error { - _, err := mediaApi.MoveFolder(ctx, param) - return err - }) -} - -func TestMedia_CopyFolder(t *testing.T) { - var err error - - var param = CopyFolderParam{ - SourceFolderPath: "/src", - DestinationPath: "dest", - } - - rs := FolderResponse{ - Data: JobIdResponse{ - JobId: "xxx", - }, - } - respBody, _ := json.Marshal(&rs.Data) - - httpTest := iktest.NewHttp(t) - - ts := httptest.NewServer(httpTest.Handler(200, string(respBody))) - defer ts.Close() - - mediaApi.Config.API.Prefix = ts.URL + "/" - response, err := mediaApi.CopyFolder(ctx, param) - - if err != nil { - t.Error(err) - } - if !cmp.Equal(response.Data.JobId, "xxx") { - t.Error(response.Data) - } - httpTest.Test("/bulkJobs/copyFolder", "POST", param) - - response, err = mediaApi.CopyFolder(ctx, CopyFolderParam{}) - - if err == nil { - t.Error("expected error") - } - errServer := iktest.NewErrorServer(t) - mediaApi.Config.API.Prefix = errServer.Url() + "/" - - errServer.TestErrors(func() error { - _, err := mediaApi.CopyFolder(ctx, param) - return err - }) -} diff --git a/api/media/media.go b/api/media/media.go deleted file mode 100644 index 411cf56..0000000 --- a/api/media/media.go +++ /dev/null @@ -1,170 +0,0 @@ -package media - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - - "github.com/imagekit-developer/imagekit-go/api" - "github.com/imagekit-developer/imagekit-go/config" - "github.com/imagekit-developer/imagekit-go/logger" -) - -// API is the main struct for media -type API struct { - Config config.Configuration - Logger *logger.Logger - Client api.HttpClient -} - -// New creates a new Media API instance from the environment variable. -func New() (*API, error) { - c, err := config.New() - if err != nil { - return nil, err - } - - return NewFromConfiguration(c) -} - -// NewFromConfiguration a new Media API instance with the given Configuration. -func NewFromConfiguration(c *config.Configuration) (*API, error) { - return &API{ - Config: *c, - Client: &http.Client{}, - Logger: logger.New(), - }, nil -} - -func (m *API) post(ctx context.Context, url string, data interface{}, ms api.MetaSetter) (*http.Response, error) { - url = api.BuildPath(m.Config.API.Prefix, url) - var err error - var body []byte - - if data != nil { - if body, err = json.Marshal(data); err != nil { - return nil, fmt.Errorf("post:marshal data: %w", err) - } - } - - req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(body)) - - if err != nil { - return nil, fmt.Errorf("post:http request: %w", err) - } - - req.Header.Set("Content-Type", "application/json") - req.SetBasicAuth(m.Config.Cloud.PrivateKey, "") - - resp, err := m.Client.Do(req.WithContext(ctx)) - defer api.DeferredBodyClose(resp) - - if err != nil { - err = fmt.Errorf("client.Do %w", err) - } - api.SetResponseMeta(resp, ms) - return resp, err -} - -func (m *API) get(ctx context.Context, url string, ms api.MetaSetter) (*http.Response, error) { - url = api.BuildPath(m.Config.API.Prefix, url) - req, err := http.NewRequest(http.MethodGet, url, nil) - - if err != nil { - return nil, err - } - - req.SetBasicAuth(m.Config.Cloud.PrivateKey, "") - - resp, err := m.Client.Do(req.WithContext(ctx)) - defer api.DeferredBodyClose(resp) - - api.SetResponseMeta(resp, ms) - - return resp, err -} - -func (m *API) delete(ctx context.Context, url string, data interface{}, ms api.MetaSetter) (*http.Response, error) { - var err error - url = api.BuildPath(m.Config.API.Prefix, url) - var body []byte - - if data != nil { - if body, err = json.Marshal(data); err != nil { - return nil, err - } - } - req, err := http.NewRequest(http.MethodDelete, url, bytes.NewBuffer(body)) - if data != nil { - req.Header.Set("Content-Type", "application/json") - } - - if err != nil { - return nil, err - } - - req.SetBasicAuth(m.Config.Cloud.PrivateKey, "") - - resp, err := m.Client.Do(req.WithContext(ctx)) - defer api.DeferredBodyClose(resp) - - api.SetResponseMeta(resp, ms) - - return resp, err -} - -func (m *API) patch(ctx context.Context, url string, data interface{}, ms api.MetaSetter) (*http.Response, error) { - url = api.BuildPath(m.Config.API.Prefix, url) - var err error - var body []byte - - if data != nil { - if body, err = json.Marshal(data); err != nil { - return nil, err - } - } - req, err := http.NewRequest(http.MethodPatch, url, bytes.NewBuffer(body)) - - if err != nil { - return nil, err - } - - req.Header.Set("Content-Type", "application/json") - req.SetBasicAuth(m.Config.Cloud.PrivateKey, "") - - resp, err := m.Client.Do(req.WithContext(ctx)) - defer api.DeferredBodyClose(resp) - - api.SetResponseMeta(resp, ms) - - return resp, err -} - -func (m *API) put(ctx context.Context, url string, data interface{}, ms api.MetaSetter) (*http.Response, error) { - url = api.BuildPath(m.Config.API.Prefix, url) - var err error - var body []byte - - if data != nil { - if body, err = json.Marshal(data); err != nil { - return nil, err - } - } - req, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(body)) - - if err != nil { - return nil, err - } - - req.Header.Set("Content-Type", "application/json") - req.SetBasicAuth(m.Config.Cloud.PrivateKey, "") - - resp, err := m.Client.Do(req.WithContext(ctx)) - defer api.DeferredBodyClose(resp) - - api.SetResponseMeta(resp, ms) - - return resp, err -} diff --git a/api/metadata/metadata.go b/api/metadata/metadata.go deleted file mode 100644 index ae49787..0000000 --- a/api/metadata/metadata.go +++ /dev/null @@ -1,427 +0,0 @@ -package metadata - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "net/http" - "time" - - neturl "net/url" - - "github.com/imagekit-developer/imagekit-go/api" - "github.com/imagekit-developer/imagekit-go/config" - "github.com/imagekit-developer/imagekit-go/logger" - "gopkg.in/validator.v2" -) - -// API is the main struct for media -type API struct { - Config config.Configuration - Logger *logger.Logger - Client api.HttpClient -} - -// New creates a new Media API instance from the environment variable. -func New() (*API, error) { - c, err := config.New() - if err != nil { - return nil, err - } - - return NewFromConfiguration(c) -} - -// NewFromConfiguration a new Media API instance with the given Configuration. -func NewFromConfiguration(c *config.Configuration) (*API, error) { - return &API{ - Config: *c, - Client: &http.Client{}, - Logger: logger.New(), - }, nil -} - -// MetadataResponse represents main struct of metadata response of the sdk -type MetadataResponse struct { - Data Metadata - api.Response -} - -// Metadata represents struct of metadata response from api -type Metadata struct { - Height int - Width int - Size int64 - Format string - HasColorProfile bool - Quality int - Density int - HasTransparency bool - PHash string - Exif Mexif -} - -type Mexif struct { - Image ImageExif - Thumbnail ThumbnailExif - Exif Exif - Gps Gps - Interoperability Interoperability - Makernote map[string]interface{} -} - -type ImageExif struct { - Make string - Model string - Orientation string - XResolution int - YResolution int - ResolutionUnit int - Software string - ModifyDate time.Time - YCbCrPositioning int - ExifOffset int - GPSInfo int -} - -type ThumbnailExif struct { - Compression int - XResolution int - YResolution int - ResolutionUnit int - ThumbnailOffset int - ThumbnailLength int -} - -type Exif struct { - ExposureTime time.Time - FNumber float32 - ExposureProgram int - ISO int - ExifVersion string - DateTimeOriginal time.Time - CreateDate time.Time - ShutterSpeedValue float32 - ApertureValue float32 - ExposureCompensation int - MeteringMode int - Flash int - FocalLength int - SubSEcTime string - SubSecTimeOriginal string - FlashpixVersion string - ColorSpace int - ExifImageWidth int - ExifImageHeight int - InteropOffset int - FocalPlaneXResolution float32 - FocalPlaneYResolution float32 - FocalPlaneResolutionUnit int - CustomRendered int - ExposureMode int - WhiteBalance int - SceneCaptutureType int -} - -type Gps struct { - GPSVersionID []int -} - -type Interoperability struct { - InteropIndex string - InteropVersion string -} - -type Schema struct { - Type string `json:"type"` //Text, Textarea, Number, Date, Boolean, SingleSelect, MultiSelect. Date value should be an ISO8601 string - - SelectOptions interface{} `json:"selectOptions,omitempty"` - DefaultValue interface{} `json:"defaultValue,omitempty"` - IsValueRequired bool `json:"isValueRequired,omitempty"` - MinValue interface{} `json:"minValue,omitempty"` - MaxValue interface{} `json:"maxValue,omitempty"` - MinLength int `json:"minLength,omitempty"` - MaxLength int `json:"maxLength,omitempty"` -} - -type CreateFieldParam struct { - Name string `json:"name"` - Label string `json:"label"` - Schema Schema `json:"schema"` -} - -type CustomField struct { - Id string - Name string - Label string - Schema Schema -} - -type CreateFieldResponse struct { - Data CustomField - api.Response -} - -type UpdateCustomFieldResponse CreateFieldResponse - -type UpdateCustomFieldParam struct { - Label string `json:"label"` - Schema Schema `json:"schema"` -} - -type CustomFieldsResponse struct { - Data []CustomField - api.Response -} - -func (m *API) get(ctx context.Context, url string, query map[string]string, ms api.MetaSetter) (*http.Response, error) { - var err error - urlObj, err := neturl.Parse(api.BuildPath(m.Config.API.Prefix, url)) - if err != nil { - return nil, err - } - - values := urlObj.Query() - for k, v := range query { - values.Set(k, v) - } - - q := values.Encode() - - sUrl := urlObj.String() - if q != "" { - sUrl = sUrl + "?" + values.Encode() - } - - req, err := http.NewRequest(http.MethodGet, sUrl, nil) - - if err != nil { - return nil, err - } - - req.SetBasicAuth(m.Config.Cloud.PrivateKey, "") - - resp, err := m.Client.Do(req.WithContext(ctx)) - defer api.DeferredBodyClose(resp) - - api.SetResponseMeta(resp, ms) - - return resp, err -} - -func (m *API) post(ctx context.Context, url string, data interface{}, ms api.MetaSetter) (*http.Response, error) { - url = api.BuildPath(m.Config.API.Prefix, url) - var err error - var body []byte - - if data != nil { - if body, err = json.Marshal(data); err != nil { - return nil, err - } - } - - req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(body)) - - if err != nil { - return nil, err - } - - req.Header.Set("Content-Type", "application/json") - req.SetBasicAuth(m.Config.Cloud.PrivateKey, "") - - resp, err := m.Client.Do(req.WithContext(ctx)) - defer api.DeferredBodyClose(resp) - - api.SetResponseMeta(resp, ms) - return resp, err -} - -func (m *API) patch(ctx context.Context, url string, data interface{}, ms api.MetaSetter) (*http.Response, error) { - url = api.BuildPath(m.Config.API.Prefix, url) - var err error - var body []byte - - if data != nil { - if body, err = json.Marshal(data); err != nil { - return nil, err - } - } - - req, err := http.NewRequest(http.MethodPatch, url, bytes.NewBuffer(body)) - - if err != nil { - return nil, err - } - - req.Header.Set("Content-Type", "application/json") - req.SetBasicAuth(m.Config.Cloud.PrivateKey, "") - - resp, err := m.Client.Do(req.WithContext(ctx)) - defer api.DeferredBodyClose(resp) - - api.SetResponseMeta(resp, ms) - return resp, err -} - -func (m *API) delete(ctx context.Context, url string, ms api.MetaSetter) (*http.Response, error) { - var err error - url = api.BuildPath(m.Config.API.Prefix, url) - - req, err := http.NewRequest(http.MethodDelete, url, nil) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/json") - - req.SetBasicAuth(m.Config.Cloud.PrivateKey, "") - - resp, err := m.Client.Do(req.WithContext(ctx)) - defer api.DeferredBodyClose(resp) - - api.SetResponseMeta(resp, ms) - return resp, err -} - -// FromFile fetches metadata of media library file -func (m *API) FromFile(ctx context.Context, fileId string) (*MetadataResponse, error) { - if fileId == "" { - return nil, errors.New("fileId can not be blank") - } - - var response = &MetadataResponse{} - - resp, err := m.get(ctx, fmt.Sprintf("files/%s/metadata", fileId), nil, response) - - if err != nil { - return response, err - } - - if resp.StatusCode != 200 { - err = response.ParseError() - } else { - err = json.Unmarshal(response.Body(), &response.Data) - } - - return response, err -} - -func (m *API) FromUrl(ctx context.Context, url string) (*MetadataResponse, error) { - var err error - if url == "" { - return nil, errors.New("url can not be blank") - } - - var response = &MetadataResponse{} - - if err != nil { - return nil, err - } - - resp, err := m.get(ctx, "metadata", map[string]string{"url": url}, response) - - if err != nil { - return response, err - } - - if resp.StatusCode != 200 { - err = response.ParseError() - } else { - err = json.Unmarshal(response.Body(), &response.Data) - } - - return response, err -} - -// CreateCustomField creates new custom metadata field -func (m *API) CreateCustomField(ctx context.Context, param CreateFieldParam) (*CreateFieldResponse, error) { - var err error - var response = &CreateFieldResponse{} - - resp, err := m.post(ctx, "customMetadataFields", param, response) - - if err != nil { - return response, err - } - - if resp.StatusCode != 201 { - err = response.ParseError() - } else { - err = json.Unmarshal(response.Body(), &response.Data) - } - - return response, err -} - -// CustomFields returns all existing custom metadata fields -func (m *API) CustomFields(ctx context.Context, includeDeleted bool) (*CustomFieldsResponse, error) { - var err error - var response = &CustomFieldsResponse{} - var flag string - - if includeDeleted == true { - flag = "true" - } else { - flag = "false" - } - - resp, err := m.get(ctx, "customMetadataFields", map[string]string{"includeDeleted": flag}, response) - - if err != nil { - return response, err - } - - if resp.StatusCode != 200 { - err = response.ParseError() - } else { - err = json.Unmarshal(response.Body(), &response.Data) - } - - return response, err -} - -// UpdateCustomField updates label or schema attributes of given custom field id -func (m *API) UpdateCustomField(ctx context.Context, fieldId string, param UpdateCustomFieldParam) (*UpdateCustomFieldResponse, error) { - var err error - var response = &UpdateCustomFieldResponse{} - - if err = validator.Validate(¶m); err != nil { - return nil, err - } - - resp, err := m.patch(ctx, "customMetadataFields/"+fieldId, param, response) - - if err != nil { - return response, err - } - - if resp.StatusCode != 200 { - err = response.ParseError() - } else { - err = json.Unmarshal(response.Body(), &response.Data) - } - - return response, err -} - -// DeleteCustomField deletes custom metadata field by given fieldId -func (m *API) DeleteCustomField(ctx context.Context, fieldId string) (*api.Response, error) { - if fieldId == "" { - return nil, errors.New("fieldId can not be blank") - } - var err error - var response = &api.Response{} - - resp, err := m.delete(ctx, "customMetadataFields/"+fieldId, response) - - if err != nil { - return response, err - } - - if resp.StatusCode != 204 { - err = response.ParseError() - } - - return response, err -} diff --git a/api/metadata/metadata_test.go b/api/metadata/metadata_test.go deleted file mode 100644 index 73b5729..0000000 --- a/api/metadata/metadata_test.go +++ /dev/null @@ -1,316 +0,0 @@ -package metadata - -import ( - "context" - "encoding/json" - "fmt" - "log" - "net/http" - "net/http/httptest" - "os" - "testing" - - "github.com/google/go-cmp/cmp" - iktest "github.com/imagekit-developer/imagekit-go/test" -) - -var ctx = context.Background() -var metadataApi *API - -func Test_New(t *testing.T) { - os.Setenv("IMAGEKIT_PRIVATE_KEY", "private_") - os.Setenv("IMAGEKIT_PUBLIC_KEY", "public_") - os.Setenv("IMAGEKIT_ENDPOINT_URL", "https://ik.imagekit.io/test/") - - defer os.Unsetenv("IMAGEKIT_PRIVATE_KEY") - defer os.Unsetenv("IMAGEKIT_PUBLIC_KEY") - defer os.Unsetenv("IMAGEKIT_ENDPOINT_URL") - - var api any - api, err := New() - - if err != nil { - t.Fatal(err) - } - - if api == nil { - t.Error("New() returned null") - } - - if _, ok := api.(*API); !ok { - t.Error("New() did not return *API") - } -} - -func TestMain(m *testing.M) { - var err error - metadataApi, err = NewFromConfiguration(iktest.Cfg) - - if err != nil { - log.Fatal(err) - } - - os.Exit(m.Run()) -} - -func getHandler(statusCode int, body string) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(statusCode) - fmt.Fprintln(w, body) - } -} - -func TestMetadata_FromFile(t *testing.T) { - var respBody = `{"height":801,"width":597,"size":59718,"format":"jpg","hasColorProfile":true,"quality":0,"density":72,"hasTransparency":false,"exif":{},"pHash":"85d07f1fe4ae8be2"}` - - var err error - var respObj = &Metadata{} - - if err = json.Unmarshal([]byte(respBody), respObj); err != nil { - t.Error(err) - } - - httpTest := iktest.NewHttp(t) - ts := httptest.NewServer(httpTest.Handler(200, string(respBody))) - defer ts.Close() - - metadataApi.Config.API.Prefix = ts.URL + "/" - - resp, err := metadataApi.FromFile(ctx, "file_id") - - if err != nil { - t.Error(err) - } - - if !cmp.Equal(resp.Data, *respObj) { - t.Errorf("\n%v\n%v\n", resp.Data, *respObj) - } - - httpTest.Test("/files/file_id/metadata", "GET", nil) - - errServer := iktest.NewErrorServer(t) - metadataApi.Config.API.Prefix = errServer.Url() + "/" - - errServer.TestErrors(func() error { - _, err := metadataApi.FromFile(ctx, "3325344545345") - return err - }) - - resp, err = metadataApi.FromFile(ctx, "") - - if err == nil { - t.Error("expected error") - } -} - -func TestMetadata_FromUrl(t *testing.T) { - var respBody = `{"height":801,"width":597,"size":59718,"format":"jpg","hasColorProfile":true,"quality":0,"density":72,"hasTransparency":false,"exif":{},"pHash":"85d07f1fe4ae8be2"}` - - var err error - var respObj = &Metadata{} - - _, err = metadataApi.FromUrl(ctx, "") - if err == nil { - t.Error("expected error") - } - - if err == nil { - t.Error("expected error") - } - if err = json.Unmarshal([]byte(respBody), respObj); err != nil { - t.Error(err) - } - - httpTest := iktest.NewHttp(t) - - ts := httptest.NewServer(httpTest.Handler(200, string(respBody))) - defer ts.Close() - - metadataApi.Config.API.Prefix = ts.URL + "/" - - resp, err := metadataApi.FromUrl(ctx, "https://ik.imagekit.io/xk1m7xkgi/default-image.jpg") - - if err != nil { - t.Error(err) - } - - if !cmp.Equal(resp.Data, *respObj) { - t.Errorf("\n%v\n%v\n", resp.Data, *respObj) - } - - httpTest.Test("/metadata?url=https%3A%2F%2Fik.imagekit.io%2Fxk1m7xkgi%2Fdefault-image.jpg", "GET", nil) - - errServer := iktest.NewErrorServer(t) - metadataApi.Config.API.Prefix = errServer.Url() + "/" - - errServer.TestErrors(func() error { - _, err := metadataApi.FromUrl(ctx, "https://ik.imagekit.io/xk1m7xkgi/default-image.jpg") - return err - }) -} - -func TestMetadata_CreateCustomField(t *testing.T) { - var respBody = `{"id":"62a8966b663ef736f841fe28","name":"speed","label":"Speed","schema":{"type":"Number","defaultValue":100,"minValue":1,"maxValue":120}}` - - var err error - var expected = &CustomField{} - - if err = json.Unmarshal([]byte(respBody), expected); err != nil { - t.Error(err) - } - - httpTest := iktest.NewHttp(t) - - ts := httptest.NewServer(httpTest.Handler(201, respBody)) - defer ts.Close() - - metadataApi.Config.API.Prefix = ts.URL + "/" - - param := CreateFieldParam{ - Name: "speed", - Label: "Speed", - Schema: Schema{ - Type: "Number", - DefaultValue: 100, - MinValue: 1, - MaxValue: 120, - }, - } - resp, err := metadataApi.CreateCustomField(ctx, param) - - if err != nil { - t.Error(err) - } - - if !cmp.Equal(resp.Data, *expected) { - t.Errorf("\n%v\n%v\n", resp.Data, *expected) - } - - httpTest.Test("/customMetadataFields", "POST", param) - - errServer := iktest.NewErrorServer(t) - metadataApi.Config.API.Prefix = errServer.Url() + "/" - - errServer.TestErrors(func() error { - _, err := metadataApi.CreateCustomField(ctx, param) - return err - }) -} - -func TestMetadata_CustomFields(t *testing.T) { - var respBody = `[{"id":"629f6b437eb0fe6f1b66d864","name":"price","label":"Price","schema":{"type":"Number","isValueRequired":false,"minValue":1,"maxValue":1000}},{"id":"629f6b6d7eb0fe344f66e1b6","name":"country","label":"Country","schema":{"type":"SingleSelect","isValueRequired":false,"selectOptions":["USA","Canada"]}},{"id":"62a8764d663ef721e93f4ea9","name":"clearance","label":"Clearance","schema":{"type":"MultiSelect","selectOptions":["one","two"]}},{"id":"62a876b1663ef7728f3f5348","name":"mileage","label":"Mileage","schema":{"type":"Number"}},{"id":"62a8966b663ef736f841fe28","name":"speed","label":"Speed","schema":{"type":"Number","defaultValue":100,"minValue":1,"maxValue":120}}]` - - var err error - var expected = []CustomField{} - - if err = json.Unmarshal([]byte(respBody), &expected); err != nil { - t.Error(err) - } - - httpTest := iktest.NewHttp(t) - - ts := httptest.NewServer(httpTest.Handler(200, respBody)) - defer ts.Close() - - metadataApi.Config.API.Prefix = ts.URL + "/" - resp, err := metadataApi.CustomFields(ctx, false) - - if err != nil { - t.Error(err) - } - if !cmp.Equal(resp.Data, expected) { - t.Errorf("%v\n%v", resp.Data, expected) - } - - httpTest.Test("/customMetadataFields?includeDeleted=false", "GET", nil) - - errServer := iktest.NewErrorServer(t) - metadataApi.Config.API.Prefix = errServer.Url() + "/" - - errServer.TestErrors(func() error { - _, err := metadataApi.CustomFields(ctx, false) - return err - }) -} - -func TestMetadata_UpdateCustomField(t *testing.T) { - var respBody = `{"id":"629f6b437eb0fe6f1b66d864","name":"price","label":"Cost","schema":{"type":"Number"}}` - - var err error - var expected = CustomField{} - - if err = json.Unmarshal([]byte(respBody), &expected); err != nil { - t.Error(err) - } - - httpTest := iktest.NewHttp(t) - ts := httptest.NewServer(httpTest.Handler(200, respBody)) - defer ts.Close() - - metadataApi.Config.API.Prefix = ts.URL + "/" - - param := UpdateCustomFieldParam{ - Label: "Cost", - } - - resp, err := metadataApi.UpdateCustomField( - ctx, - "file_id", - param, - ) - - if err != nil { - t.Error(err) - } - if !cmp.Equal(resp.Data, expected) { - t.Errorf("%v\n%v", resp.Data, expected) - } - - httpTest.Test("/customMetadataFields/file_id", "PATCH", param) - - errServer := iktest.NewErrorServer(t) - metadataApi.Config.API.Prefix = errServer.Url() + "/" - - errServer.TestErrors(func() error { - _, err := metadataApi.UpdateCustomField( - ctx, - "629f6b437eb0fe6f1b66d864", - UpdateCustomFieldParam{ - Label: "Cost", - }, - ) - return err - }) -} - -func TestMetadata_DeleteCustomField(t *testing.T) { - var respBody = `` - var err error - - if _, err := metadataApi.DeleteCustomField(ctx, ""); err == nil { - t.Error("expected error") - } - - httpTest := iktest.NewHttp(t) - - ts := httptest.NewServer(httpTest.Handler(204, respBody)) - defer ts.Close() - - metadataApi.Config.API.Prefix = ts.URL + "/" - - _, err = metadataApi.DeleteCustomField(ctx, "file_id") - if err != nil { - log.Println("got error") - t.Error(err) - } - - httpTest.Test("/customMetadataFields/file_id", "DELETE", nil) - - errServer := iktest.NewErrorServer(t) - metadataApi.Config.API.Prefix = errServer.Url() + "/" - - errServer.TestErrors(func() error { - _, err = metadataApi.DeleteCustomField(ctx, "62a8966b663ef736f841fe28") - return err - }) -} diff --git a/api/uploader/upload_asset.go b/api/uploader/upload_asset.go deleted file mode 100644 index 9cda9d9..0000000 --- a/api/uploader/upload_asset.go +++ /dev/null @@ -1,96 +0,0 @@ -package uploader - -import ( - "context" - "encoding/json" - "errors" - - "github.com/imagekit-developer/imagekit-go/api" - "github.com/imagekit-developer/imagekit-go/api/extension" -) - -// UploadParam defines upload parameters -type UploadParam struct { - FileName string `json:"fileName"` - UseUniqueFileName *bool `json:"useUniqueFileName,omitempty"` - Tags string `json:"tags,omitempty"` - Folder string `json:"folder,omitempty"` // default value: / - IsPrivateFile *bool `json:"isPrivateFile,omitempty"` // default: false - CustomCoordinates string `json:"customCoordinates,omitempty"` - ResponseFields string `json:"responseFields,omitempty"` - ExtensionsJson string `json:"extensions,omitempty"` - - Extensions []extension.IExtension `json:"-"` - WebhookUrl string `json:"webhookUrl,omitempty"` - OverwriteFile *bool `json:"overwriteFile,omitempty"` - OverwriteAITags *bool `json:"overwriteAITags,omitempty"` - OverwriteTags *bool `json:"overwriteTags,omitempty"` - OverwriteCustomMetadata *bool `json:"overwriteCustomMetadata,omitempty"` - CustomMetadata map[string]any `json:"customMetadata,omitempty"` -} - -type UploadResult struct { - FileId string `json:"fileId"` - Name string `json:"name"` - Url string `json:"url"` - ThumbnailUrl string `json:"thumbnailUrl"` - Height int `json:"height"` - Width int `json:"Width"` - Size uint64 `json:"size"` - FilePath string `json:"filePath"` - AITags []map[string]any `json:"AITags"` - VersionInfo map[string]string `json:"versionInfo"` -} - -type UploadResponse struct { - Data UploadResult - api.Response -} - -// Upload uploads an asset to a imagekit account. -// -// The asset can be: -// * the actual data (io.Reader) -// * the Data URI (Base64 encoded), max ~60 MB (62,910,000 chars) -// * the remote FTP, HTTP or HTTPS URL address of an existing file -// -// https://docs.imagekit.io/api-reference/upload-file-api/server-side-file-upload -func (u *API) Upload(ctx context.Context, file interface{}, param UploadParam) (*UploadResponse, error) { - var err error - - if param.FileName == "" { - return nil, errors.New("Upload: Filename is required") - } - - if param.Extensions != nil { - bt, err := json.Marshal(param.Extensions) - if err != nil { - return nil, err - } - param.ExtensionsJson = string(bt) - } - - formParams, err := api.StructToParams(param) - - if err != nil { - return nil, err - } - - response := &UploadResponse{} - - resp, err := u.postFile(ctx, file, formParams) - defer api.DeferredBodyClose(resp) - - api.SetResponseMeta(resp, response) - - if err != nil { - return response, err - } - - if resp.StatusCode != 200 { - err = response.ParseError() - } else { - err = json.Unmarshal(response.Body(), &response.Data) - } - return response, err -} diff --git a/api/uploader/uploader.go b/api/uploader/uploader.go deleted file mode 100644 index 0d89db7..0000000 --- a/api/uploader/uploader.go +++ /dev/null @@ -1,134 +0,0 @@ -package uploader - -import ( - "bytes" - "context" - "errors" - "io" - "mime/multipart" - "net/http" - "net/url" - "time" - - "github.com/imagekit-developer/imagekit-go/api" - "github.com/imagekit-developer/imagekit-go/config" - "github.com/imagekit-developer/imagekit-go/logger" -) - -// API is the upload feature main struct -type API struct { - Config config.Configuration - Logger *logger.Logger - Client api.HttpClient -} - -// New creates a new Uploader API instance from environment variables. -func New() (*API, error) { - c, err := config.New() - if err != nil { - return nil, err - } - - return NewFromConfiguration(c) -} - -// NewFromConfiguration creates a new Upload API instance with the given Configuration. -func NewFromConfiguration(c *config.Configuration) (*API, error) { - return &API{ - Config: *c, - Client: &http.Client{}, - Logger: logger.New(), - }, nil -} - -// postFile uploads file with url.Values parameters -func (u *API) postFile(ctx context.Context, file interface{}, formParams url.Values) (*http.Response, error) { - uploadEndpoint := api.BuildPath("files", "upload") - - switch fileValue := file.(type) { - case string: - // Can be URL, Base64 encoded string, etc. - formParams.Add("file", fileValue) - return u.postForm(ctx, uploadEndpoint, formParams) - case io.Reader: - return u.postIOReader(ctx, uploadEndpoint, fileValue, formParams, map[string]string{}) - - default: - return nil, errors.New("unsupported file type") - } -} - -// postIOReader uploads file using io.Reader -func (u *API) postIOReader(ctx context.Context, urlPath string, reader io.Reader, formParams url.Values, headers map[string]string) (*http.Response, error) { - bodyBuf := new(bytes.Buffer) - formWriter := multipart.NewWriter(bodyBuf) - - headers["Content-Type"] = formWriter.FormDataContentType() - - for key, val := range formParams { - _ = formWriter.WriteField(key, val[0]) - } - - partWriter, err := formWriter.CreateFormFile("file", formParams.Get("fileName")) - if err != nil { - return nil, err - } - - if _, err = io.Copy(partWriter, reader); err != nil { - return nil, err - } - - if err = formWriter.Close(); err != nil { - return nil, err - } - - if u.Config.API.UploadTimeout != 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, time.Duration(u.Config.API.UploadTimeout)*time.Second) - defer cancel() - } - - return u.postBody(ctx, urlPath, bodyBuf, headers) -} - -func (u *API) postBody(ctx context.Context, urlPath string, bodyBuf *bytes.Buffer, headers map[string]string) (*http.Response, error) { - - req, err := http.NewRequest(http.MethodPost, - u.Config.API.UploadPrefix+urlPath, - bodyBuf, - ) - - if err != nil { - return nil, err - } - - req.SetBasicAuth(u.Config.Cloud.PrivateKey, "") - - for key, val := range headers { - req.Header.Add(key, val) - } - - req = req.WithContext(ctx) - - return u.Client.Do(req) -} - -func (u *API) postForm(ctx context.Context, urlPath string, formParams url.Values) (*http.Response, error) { - - bodyBuf := new(bytes.Buffer) - writer := multipart.NewWriter(bodyBuf) - - for k, _ := range formParams { - writer.WriteField(k, formParams.Get(k)) - } - err := writer.Close() - if err != nil { - return nil, err - } - - h := map[string]string{"Content-Type": writer.FormDataContentType()} - ctx, cancel := context.WithTimeout(ctx, time.Duration(u.Config.API.Timeout)*time.Second) - defer cancel() - - return u.postBody(ctx, urlPath, bodyBuf, h) -} diff --git a/api/uploader/uploader_test.go b/api/uploader/uploader_test.go deleted file mode 100644 index 79fd6f7..0000000 --- a/api/uploader/uploader_test.go +++ /dev/null @@ -1,267 +0,0 @@ -package uploader - -import ( - "bytes" - "context" - "encoding/json" - "io" - "log" - "mime" - "mime/multipart" - "net/http" - "net/http/httptest" - "net/url" - "os" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/imagekit-developer/imagekit-go/api" - "github.com/imagekit-developer/imagekit-go/api/extension" - iktest "github.com/imagekit-developer/imagekit-go/test" -) - -var ctx = context.Background() -var header = http.Header{} -var ImageFileData []byte - -func init() { - header.Add("Content-Type", "text/plain; charset=utf-8") - header.Add("Content-Length", "600") - header.Add("Date", "Sat, 04 Jun 2022 06:50:21 GMT") - - file, _ := os.Open(iktest.ImageFilePath) - ImageFileData, _ = io.ReadAll(file) -} - -var file = &UploadResult{ - FileId: "6299dcfb124cdf0dffaf2dfd", - Name: "new-york-cityscape-buildings_A4zxKJbrL.jpg", - Url: "https://ik.imagekit.io/dk1m7xkgi/new-york-cityscape-buildings_A4zxKJbrL.jpg", - ThumbnailUrl: "https://ik.imagekit.io/dk1m7xkgi/tr:n-ik_ml_thumbnail/new-york-cityscape-buildings_A4zxKJbrL.jpg", - Height: 1080, - Width: 1920, - Size: 1468522, - FilePath: "/new-york-cityscape-buildings_A4zxKJbrL.jpg", - AITags: nil, - VersionInfo: map[string]string{ - "id": "6299dcfb124cdf0dffaf2dfd", "name": "Version 1", - }, -} - -func newUploader(url string) (*API, error) { - uploader, err := NewFromConfiguration(iktest.Cfg) - - if err != nil { - return nil, err - } - - uploader.Config.API.UploadPrefix = url - return uploader, nil - -} - -func Test_New(t *testing.T) { - os.Setenv("IMAGEKIT_PRIVATE_KEY", "private_") - os.Setenv("IMAGEKIT_PUBLIC_KEY", "public_") - os.Setenv("IMAGEKIT_ENDPOINT_URL", "https://ik.imagekit.io/test/") - - defer os.Unsetenv("IMAGEKIT_PRIVATE_KEY") - defer os.Unsetenv("IMAGEKIT_PUBLIC_KEY") - defer os.Unsetenv("IMAGEKIT_ENDPOINT_URL") - - var api any - api, err := New() - - if err != nil { - t.Fatal(err) - } - - if api == nil { - t.Error("New() returned null") - } - - if _, ok := api.(*API); !ok { - t.Error("New() did not return *API") - } -} - -func TestUploader(t *testing.T) { - resultJson, err := json.Marshal(file) - if err != nil { - t.Error(err) - } - - reader, err := os.Open(iktest.ImageFilePath) - - if err != nil { - t.Fatal(err) - } - - var extensions = []extension.IExtension{ - extension.NewAutoTag(extension.GoogleAutoTag, 50, 10), - extension.NewRemoveBg(extension.RemoveBgOption{ - AddShadow: true, - SemiTransparency: true, - BgColor: "#553333", - BgImageUrl: "http://test/test.jpg", - }), - } - - param := UploadParam{ - FileName: "new-york-cityscape-buildings_A4zxKJbrL.jpg", - UseUniqueFileName: api.Bool(false), - Tags: "tag_1,tag_2", - Folder: "/natural", - IsPrivateFile: api.Bool(false), - CustomCoordinates: "11,100,400,500", - ResponseFields: "tags,customCoordinates,isPrivateFile", - Extensions: extensions, - WebhookUrl: "http://test/test", - OverwriteFile: api.Bool(true), - OverwriteAITags: api.Bool(true), - OverwriteTags: api.Bool(true), - OverwriteCustomMetadata: api.Bool(true), - CustomMetadata: map[string]any{ - "Cost": 100, - }, - } - - var formStr = `{"customCoordinates":"11,100,400,500","customMetadata":"{\"Cost\":100}","extensions":"[{\"name\":\"google-auto-tagging\",\"minConfidence\":50,\"maxTags\":10},{\"name\":\"remove-bg\",\"options\":{\"add_shadow\":true,\"semitransparency\":true,\"bg_color\":\"#553333\",\"bg_image_url\":\"http://test/test.jpg\"}}]","file":"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7","fileName":"new-york-cityscape-buildings_A4zxKJbrL.jpg","folder":"/natural","isPrivateFile":"false","overwriteAITags":"true","overwriteCustomMetadata":"true","overwriteFile":"true","overwriteTags":"true","responseFields":"tags,customCoordinates,isPrivateFile","tags":"tag_1,tag_2","useUniqueFileName":"false","webhookUrl":"http://test/test"}` - - var readerForm = `{"customCoordinates":"11,100,400,500","customMetadata":"{\"Cost\":100}","extensions":"[{\"name\":\"google-auto-tagging\",\"minConfidence\":50,\"maxTags\":10},{\"name\":\"remove-bg\",\"options\":{\"add_shadow\":true,\"semitransparency\":true,\"bg_color\":\"#553333\",\"bg_image_url\":\"http://test/test.jpg\"}}]","fileName":"new-york-cityscape-buildings_A4zxKJbrL.jpg","folder":"/natural","isPrivateFile":"false","overwriteAITags":"true","overwriteCustomMetadata":"true","overwriteFile":"true","overwriteTags":"true","responseFields":"tags,customCoordinates,isPrivateFile","tags":"tag_1,tag_2","useUniqueFileName":"false","webhookUrl":"http://test/test"}` - - var cases = map[string]struct { - file any - resp string - param UploadParam - result *UploadResult - shouldFail bool - formStr string - }{ - "base64file": { - file: iktest.Base64Image, - resp: string(resultJson), - param: param, - result: file, - formStr: formStr, - }, - "io-reader": { - file: reader, - resp: string(resultJson), - param: param, - result: file, - formStr: readerForm, - }, - "missing-filename": { - file: reader, - resp: string(resultJson), - param: UploadParam{}, - result: file, - shouldFail: true, - }, - "invalid file param": { - file: []int{}, - resp: "", - param: UploadParam{}, - result: nil, - shouldFail: true, - }, - } - - for name, test := range cases { - t.Run(name, func(t *testing.T) { - httpTest := iktest.NewHttp(t) - - ts := httptest.NewServer(httpTest.Handler(200, string(resultJson))) - defer ts.Close() - - uploader, err := newUploader(ts.URL + "/") - if err != nil { - t.Fatal(err) - } - - resp, err := uploader.Upload(ctx, test.file, test.param) - - if !test.shouldFail && err != nil { - t.Error(err) - } - - if test.shouldFail { - if err == nil { - t.Error("err is nil") - } else if err.Error() != "Upload: Filename is required" { - t.Error("wrong error message", err.Error()) - } - return - } - - _, params, err := mime.ParseMediaType(httpTest.Req.Header.Get("Content-Type")) - - if err != nil { - t.Fatal(err) - } - - mr := multipart.NewReader(bytes.NewReader(httpTest.Body), params["boundary"]) - - form, err := mr.ReadForm(1024 * 2) - var f = map[string]string{} - - for k, v := range form.Value { - f[k] = v[0] - } - str, err := json.Marshal(f) - - if !cmp.Equal(string(str), test.formStr) { - t.Error("invalid form values") - log.Println(test.formStr) - log.Println(string(str)) - } - if form.Value["fileName"][0] != test.param.FileName { - t.Error("invalid filename") - } - - switch test.file.(type) { - case string: - if test.file != form.Value["file"][0] { - t.Error("unexpected file value") - } - default: - file, err := form.File["file"][0].Open() - - if err != nil { - t.Fatal(err) - } - data, _ := io.ReadAll(file) - - if !cmp.Equal(data, ImageFileData) { - t.Error(name + ": unexpected file submitted") - } - } - - if !cmp.Equal(resp.Data, *test.result) { - t.Errorf("\n%v\n%v\n", resp.Data, *test.result) - } - }) - } - - errServer := iktest.NewErrorServer(t) - uploader, err := newUploader(errServer.Url() + "/") - errServer.TestErrors(func() error { - _, err := uploader.Upload(ctx, reader, param) - return err - }) -} - -func Test_postFile(t *testing.T) { - uploader, err := newUploader("/") - if err != nil { - t.Fatal(err) - } - - _, err = uploader.postFile(ctx, 5, url.Values{}) - - if err == nil { - t.Error("expected error") - } - -} diff --git a/asset.go b/asset.go new file mode 100644 index 0000000..d0d6584 --- /dev/null +++ b/asset.go @@ -0,0 +1,256 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + "time" + + "github.com/imagekit-developer/imagekit-go/internal/apijson" + "github.com/imagekit-developer/imagekit-go/internal/apiquery" + "github.com/imagekit-developer/imagekit-go/internal/requestconfig" + "github.com/imagekit-developer/imagekit-go/option" + "github.com/imagekit-developer/imagekit-go/packages/param" + "github.com/imagekit-developer/imagekit-go/packages/respjson" +) + +// AssetService contains methods and other services that help with interacting with +// the ImageKit API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewAssetService] method instead. +type AssetService struct { + Options []option.RequestOption +} + +// NewAssetService generates a new service that applies the given options to each +// request. These options are applied after the parent client's options (if there +// is one), and before any request-specific options. +func NewAssetService(opts ...option.RequestOption) (r AssetService) { + r = AssetService{} + r.Options = opts + return +} + +// This API can list all the uploaded files and folders in your ImageKit.io media +// library. In addition, you can fine-tune your query by specifying various filters +// by generating a query string in a Lucene-like syntax and provide this generated +// string as the value of the `searchQuery`. +func (r *AssetService) List(ctx context.Context, query AssetListParams, opts ...option.RequestOption) (res *[]AssetListResponseUnion, err error) { + opts = append(r.Options[:], opts...) + path := "v1/files" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) + return +} + +// AssetListResponseUnion contains all possible properties and values from [File], +// [Folder]. +// +// Use the [AssetListResponseUnion.AsAny] method to switch on the variant. +// +// Use the methods beginning with 'As' to cast the union to one of its variants. +type AssetListResponseUnion struct { + // This field is from variant [File]. + AITags []FileAITag `json:"AITags"` + CreatedAt time.Time `json:"createdAt"` + // This field is from variant [File]. + CustomCoordinates string `json:"customCoordinates"` + // This field is from variant [File]. + CustomMetadata map[string]any `json:"customMetadata"` + // This field is from variant [File]. + Description string `json:"description"` + // This field is from variant [File]. + FileID string `json:"fileId"` + // This field is from variant [File]. + FilePath string `json:"filePath"` + // This field is from variant [File]. + FileType string `json:"fileType"` + // This field is from variant [File]. + HasAlpha bool `json:"hasAlpha"` + // This field is from variant [File]. + Height float64 `json:"height"` + // This field is from variant [File]. + IsPrivateFile bool `json:"isPrivateFile"` + // This field is from variant [File]. + IsPublished bool `json:"isPublished"` + // This field is from variant [File]. + Mime string `json:"mime"` + Name string `json:"name"` + // This field is from variant [File]. + Size float64 `json:"size"` + // This field is from variant [File]. + Tags []string `json:"tags"` + // This field is from variant [File]. + Thumbnail string `json:"thumbnail"` + // Any of nil, "folder". + Type string `json:"type"` + UpdatedAt time.Time `json:"updatedAt"` + // This field is from variant [File]. + URL string `json:"url"` + // This field is from variant [File]. + VersionInfo FileVersionInfo `json:"versionInfo"` + // This field is from variant [File]. + Width float64 `json:"width"` + // This field is from variant [Folder]. + FolderID string `json:"folderId"` + // This field is from variant [Folder]. + FolderPath string `json:"folderPath"` + JSON struct { + AITags respjson.Field + CreatedAt respjson.Field + CustomCoordinates respjson.Field + CustomMetadata respjson.Field + Description respjson.Field + FileID respjson.Field + FilePath respjson.Field + FileType respjson.Field + HasAlpha respjson.Field + Height respjson.Field + IsPrivateFile respjson.Field + IsPublished respjson.Field + Mime respjson.Field + Name respjson.Field + Size respjson.Field + Tags respjson.Field + Thumbnail respjson.Field + Type respjson.Field + UpdatedAt respjson.Field + URL respjson.Field + VersionInfo respjson.Field + Width respjson.Field + FolderID respjson.Field + FolderPath respjson.Field + raw string + } `json:"-"` +} + +func (u AssetListResponseUnion) AsFileFileVersion() (v File) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u AssetListResponseUnion) AsFolder() (v Folder) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +// Returns the unmodified JSON received from the API +func (u AssetListResponseUnion) RawJSON() string { return u.JSON.raw } + +func (r *AssetListResponseUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type AssetListParams struct { + // The maximum number of results to return in response. + Limit param.Opt[int64] `query:"limit,omitzero" json:"-"` + // Folder path if you want to limit the search within a specific folder. For + // example, `/sales-banner/` will only search in folder sales-banner. + // + // Note : If your use case involves searching within a folder as well as its + // subfolders, you can use `path` parameter in `searchQuery` with appropriate + // operator. Checkout + // [Supported parameters](/docs/api-reference/digital-asset-management-dam/list-and-search-assets#supported-parameters) + // for more information. + Path param.Opt[string] `query:"path,omitzero" json:"-"` + // Query string in a Lucene-like query language e.g. `createdAt > "7d"`. + // + // Note : When the searchQuery parameter is present, the following query parameters + // will have no effect on the result: + // + // 1. `tags` + // 2. `type` + // 3. `name` + // + // [Learn more](/docs/api-reference/digital-asset-management-dam/list-and-search-assets#advanced-search-queries) + // from examples. + SearchQuery param.Opt[string] `query:"searchQuery,omitzero" json:"-"` + // The number of results to skip before returning results. + Skip param.Opt[int64] `query:"skip,omitzero" json:"-"` + // Filter results by file type. + // + // - `all` — include all file types + // - `image` — include only image files + // - `non-image` — include only non-image files (e.g., JS, CSS, video) + // + // Any of "all", "image", "non-image". + FileType AssetListParamsFileType `query:"fileType,omitzero" json:"-"` + // Sort the results by one of the supported fields in ascending or descending + // order. + // + // Any of "ASC_NAME", "DESC_NAME", "ASC_CREATED", "DESC_CREATED", "ASC_UPDATED", + // "DESC_UPDATED", "ASC_HEIGHT", "DESC_HEIGHT", "ASC_WIDTH", "DESC_WIDTH", + // "ASC_SIZE", "DESC_SIZE", "ASC_RELEVANCE", "DESC_RELEVANCE". + Sort AssetListParamsSort `query:"sort,omitzero" json:"-"` + // Filter results by asset type. + // + // - `file` — returns only files + // - `file-version` — returns specific file versions + // - `folder` — returns only folders + // - `all` — returns both files and folders (excludes `file-version`) + // + // Any of "file", "file-version", "folder", "all". + Type AssetListParamsType `query:"type,omitzero" json:"-"` + paramObj +} + +// URLQuery serializes [AssetListParams]'s query parameters as `url.Values`. +func (r AssetListParams) URLQuery() (v url.Values, err error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + +// Filter results by file type. +// +// - `all` — include all file types +// - `image` — include only image files +// - `non-image` — include only non-image files (e.g., JS, CSS, video) +type AssetListParamsFileType string + +const ( + AssetListParamsFileTypeAll AssetListParamsFileType = "all" + AssetListParamsFileTypeImage AssetListParamsFileType = "image" + AssetListParamsFileTypeNonImage AssetListParamsFileType = "non-image" +) + +// Sort the results by one of the supported fields in ascending or descending +// order. +type AssetListParamsSort string + +const ( + AssetListParamsSortAscName AssetListParamsSort = "ASC_NAME" + AssetListParamsSortDescName AssetListParamsSort = "DESC_NAME" + AssetListParamsSortAscCreated AssetListParamsSort = "ASC_CREATED" + AssetListParamsSortDescCreated AssetListParamsSort = "DESC_CREATED" + AssetListParamsSortAscUpdated AssetListParamsSort = "ASC_UPDATED" + AssetListParamsSortDescUpdated AssetListParamsSort = "DESC_UPDATED" + AssetListParamsSortAscHeight AssetListParamsSort = "ASC_HEIGHT" + AssetListParamsSortDescHeight AssetListParamsSort = "DESC_HEIGHT" + AssetListParamsSortAscWidth AssetListParamsSort = "ASC_WIDTH" + AssetListParamsSortDescWidth AssetListParamsSort = "DESC_WIDTH" + AssetListParamsSortAscSize AssetListParamsSort = "ASC_SIZE" + AssetListParamsSortDescSize AssetListParamsSort = "DESC_SIZE" + AssetListParamsSortAscRelevance AssetListParamsSort = "ASC_RELEVANCE" + AssetListParamsSortDescRelevance AssetListParamsSort = "DESC_RELEVANCE" +) + +// Filter results by asset type. +// +// - `file` — returns only files +// - `file-version` — returns specific file versions +// - `folder` — returns only folders +// - `all` — returns both files and folders (excludes `file-version`) +type AssetListParamsType string + +const ( + AssetListParamsTypeFile AssetListParamsType = "file" + AssetListParamsTypeFileVersion AssetListParamsType = "file-version" + AssetListParamsTypeFolder AssetListParamsType = "folder" + AssetListParamsTypeAll AssetListParamsType = "all" +) diff --git a/asset_test.go b/asset_test.go new file mode 100644 index 0000000..f86f3a0 --- /dev/null +++ b/asset_test.go @@ -0,0 +1,46 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/imagekit-developer/imagekit-go" + "github.com/imagekit-developer/imagekit-go/internal/testutil" + "github.com/imagekit-developer/imagekit-go/option" +) + +func TestAssetListWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Assets.List(context.TODO(), imagekit.AssetListParams{ + FileType: imagekit.AssetListParamsFileTypeAll, + Limit: imagekit.Int(1), + Path: imagekit.String("path"), + SearchQuery: imagekit.String("searchQuery"), + Skip: imagekit.Int(0), + Sort: imagekit.AssetListParamsSortAscName, + Type: imagekit.AssetListParamsTypeFile, + }) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/beta.go b/beta.go new file mode 100644 index 0000000..c81a886 --- /dev/null +++ b/beta.go @@ -0,0 +1,28 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit + +import ( + "github.com/imagekit-developer/imagekit-go/option" +) + +// BetaService contains methods and other services that help with interacting with +// the ImageKit API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewBetaService] method instead. +type BetaService struct { + Options []option.RequestOption + V2 BetaV2Service +} + +// NewBetaService generates a new service that applies the given options to each +// request. These options are applied after the parent client's options (if there +// is one), and before any request-specific options. +func NewBetaService(opts ...option.RequestOption) (r BetaService) { + r = BetaService{} + r.Options = opts + r.V2 = NewBetaV2Service(opts...) + return +} diff --git a/betav2.go b/betav2.go new file mode 100644 index 0000000..6f72dcc --- /dev/null +++ b/betav2.go @@ -0,0 +1,28 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit + +import ( + "github.com/imagekit-developer/imagekit-go/option" +) + +// BetaV2Service contains methods and other services that help with interacting +// with the ImageKit API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewBetaV2Service] method instead. +type BetaV2Service struct { + Options []option.RequestOption + Files BetaV2FileService +} + +// NewBetaV2Service generates a new service that applies the given options to each +// request. These options are applied after the parent client's options (if there +// is one), and before any request-specific options. +func NewBetaV2Service(opts ...option.RequestOption) (r BetaV2Service) { + r = BetaV2Service{} + r.Options = opts + r.Files = NewBetaV2FileService(opts...) + return +} diff --git a/betav2file.go b/betav2file.go new file mode 100644 index 0000000..ce08988 --- /dev/null +++ b/betav2file.go @@ -0,0 +1,599 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit + +import ( + "bytes" + "context" + "io" + "mime/multipart" + "net/http" + + "github.com/imagekit-developer/imagekit-go/internal/apiform" + "github.com/imagekit-developer/imagekit-go/internal/apijson" + "github.com/imagekit-developer/imagekit-go/internal/requestconfig" + "github.com/imagekit-developer/imagekit-go/option" + "github.com/imagekit-developer/imagekit-go/packages/param" + "github.com/imagekit-developer/imagekit-go/packages/respjson" + "github.com/imagekit-developer/imagekit-go/shared" + "github.com/imagekit-developer/imagekit-go/shared/constant" +) + +// BetaV2FileService contains methods and other services that help with interacting +// with the ImageKit API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewBetaV2FileService] method instead. +type BetaV2FileService struct { + Options []option.RequestOption +} + +// NewBetaV2FileService generates a new service that applies the given options to +// each request. These options are applied after the parent client's options (if +// there is one), and before any request-specific options. +func NewBetaV2FileService(opts ...option.RequestOption) (r BetaV2FileService) { + r = BetaV2FileService{} + r.Options = opts + return +} + +// The V2 API enhances security by verifying the entire payload using JWT. This API +// is in beta. +// +// ImageKit.io allows you to upload files directly from both the server and client +// sides. For server-side uploads, private API key authentication is used. For +// client-side uploads, generate a one-time `token` from your secure backend using +// private API. +// [Learn more](/docs/api-reference/upload-file/upload-file-v2#how-to-implement-secure-client-side-file-upload) +// about how to implement secure client-side file upload. +// +// **File size limit** \ +// On the free plan, the maximum upload file sizes are 20MB for images, audio, and raw +// files, and 100MB for videos. On the paid plan, these limits increase to 40MB for +// images, audio, and raw files, and 2GB for videos. These limits can be further increased +// with higher-tier plans. +// +// **Version limit** \ +// A file can have a maximum of 100 versions. +// +// **Demo applications** +// +// - A full-fledged +// [upload widget using Uppy](https://github.com/imagekit-samples/uppy-uploader), +// supporting file selections from local storage, URL, Dropbox, Google Drive, +// Instagram, and more. +// - [Quick start guides](/docs/quick-start-guides) for various frameworks and +// technologies. +func (r *BetaV2FileService) Upload(ctx context.Context, body BetaV2FileUploadParams, opts ...option.RequestOption) (res *BetaV2FileUploadResponse, err error) { + opts = append(r.Options[:], opts...) + opts = append([]option.RequestOption{option.WithBaseURL("https://upload.imagekit.io/")}, opts...) + path := "api/v2/files/upload" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +// Object containing details of a successful upload. +type BetaV2FileUploadResponse struct { + // An array of tags assigned to the uploaded file by auto tagging. + AITags []BetaV2FileUploadResponseAITag `json:"AITags,nullable"` + // The audio codec used in the video (only for video). + AudioCodec string `json:"audioCodec"` + // The bit rate of the video in kbps (only for video). + BitRate int64 `json:"bitRate"` + // Value of custom coordinates associated with the image in the format + // `x,y,width,height`. If `customCoordinates` are not defined, then it is `null`. + // Send `customCoordinates` in `responseFields` in API request to get the value of + // this field. + CustomCoordinates string `json:"customCoordinates,nullable"` + // A key-value data associated with the asset. Use `responseField` in API request + // to get `customMetadata` in the upload API response. Before setting any custom + // metadata on an asset, you have to create the field using custom metadata fields + // API. Send `customMetadata` in `responseFields` in API request to get the value + // of this field. + CustomMetadata map[string]any `json:"customMetadata"` + // Optional text to describe the contents of the file. Can be set by the user or + // the ai-auto-description extension. + Description string `json:"description"` + // The duration of the video in seconds (only for video). + Duration int64 `json:"duration"` + // Consolidated embedded metadata associated with the file. It includes exif, iptc, + // and xmp data. Send `embeddedMetadata` in `responseFields` in API request to get + // embeddedMetadata in the upload API response. + EmbeddedMetadata map[string]any `json:"embeddedMetadata"` + // Extension names with their processing status at the time of completion of the + // request. It could have one of the following status values: + // + // `success`: The extension has been successfully applied. `failed`: The extension + // has failed and will not be retried. `pending`: The extension will finish + // processing in some time. On completion, the final status (success / failed) will + // be sent to the `webhookUrl` provided. + // + // If no extension was requested, then this parameter is not returned. + ExtensionStatus BetaV2FileUploadResponseExtensionStatus `json:"extensionStatus"` + // Unique fileId. Store this fileld in your database, as this will be used to + // perform update action on this file. + FileID string `json:"fileId"` + // The relative path of the file in the media library e.g. + // `/marketing-assets/new-banner.jpg`. + FilePath string `json:"filePath"` + // Type of the uploaded file. Possible values are `image`, `non-image`. + FileType string `json:"fileType"` + // Height of the image in pixels (Only for images) + Height float64 `json:"height"` + // Is the file marked as private. It can be either `true` or `false`. Send + // `isPrivateFile` in `responseFields` in API request to get the value of this + // field. + IsPrivateFile bool `json:"isPrivateFile"` + // Is the file published or in draft state. It can be either `true` or `false`. + // Send `isPublished` in `responseFields` in API request to get the value of this + // field. + IsPublished bool `json:"isPublished"` + // Legacy metadata. Send `metadata` in `responseFields` in API request to get + // metadata in the upload API response. + Metadata Metadata `json:"metadata"` + // Name of the asset. + Name string `json:"name"` + // Size of the image file in Bytes. + Size float64 `json:"size"` + // The array of tags associated with the asset. If no tags are set, it will be + // `null`. Send `tags` in `responseFields` in API request to get the value of this + // field. + Tags []string `json:"tags,nullable"` + // In the case of an image, a small thumbnail URL. + ThumbnailURL string `json:"thumbnailUrl"` + // A publicly accessible URL of the file. + URL string `json:"url"` + // An object containing the file or file version's `id` (versionId) and `name`. + VersionInfo BetaV2FileUploadResponseVersionInfo `json:"versionInfo"` + // The video codec used in the video (only for video). + VideoCodec string `json:"videoCodec"` + // Width of the image in pixels (Only for Images) + Width float64 `json:"width"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + AITags respjson.Field + AudioCodec respjson.Field + BitRate respjson.Field + CustomCoordinates respjson.Field + CustomMetadata respjson.Field + Description respjson.Field + Duration respjson.Field + EmbeddedMetadata respjson.Field + ExtensionStatus respjson.Field + FileID respjson.Field + FilePath respjson.Field + FileType respjson.Field + Height respjson.Field + IsPrivateFile respjson.Field + IsPublished respjson.Field + Metadata respjson.Field + Name respjson.Field + Size respjson.Field + Tags respjson.Field + ThumbnailURL respjson.Field + URL respjson.Field + VersionInfo respjson.Field + VideoCodec respjson.Field + Width respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r BetaV2FileUploadResponse) RawJSON() string { return r.JSON.raw } +func (r *BetaV2FileUploadResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type BetaV2FileUploadResponseAITag struct { + // Confidence score of the tag. + Confidence float64 `json:"confidence"` + // Name of the tag. + Name string `json:"name"` + // Array of `AITags` associated with the image. If no `AITags` are set, it will be + // null. These tags can be added using the `google-auto-tagging` or + // `aws-auto-tagging` extensions. + Source string `json:"source"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Confidence respjson.Field + Name respjson.Field + Source respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r BetaV2FileUploadResponseAITag) RawJSON() string { return r.JSON.raw } +func (r *BetaV2FileUploadResponseAITag) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Extension names with their processing status at the time of completion of the +// request. It could have one of the following status values: +// +// `success`: The extension has been successfully applied. `failed`: The extension +// has failed and will not be retried. `pending`: The extension will finish +// processing in some time. On completion, the final status (success / failed) will +// be sent to the `webhookUrl` provided. +// +// If no extension was requested, then this parameter is not returned. +type BetaV2FileUploadResponseExtensionStatus struct { + // Any of "success", "pending", "failed". + AIAutoDescription string `json:"ai-auto-description"` + // Any of "success", "pending", "failed". + AwsAutoTagging string `json:"aws-auto-tagging"` + // Any of "success", "pending", "failed". + GoogleAutoTagging string `json:"google-auto-tagging"` + // Any of "success", "pending", "failed". + RemoveBg string `json:"remove-bg"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + AIAutoDescription respjson.Field + AwsAutoTagging respjson.Field + GoogleAutoTagging respjson.Field + RemoveBg respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r BetaV2FileUploadResponseExtensionStatus) RawJSON() string { return r.JSON.raw } +func (r *BetaV2FileUploadResponseExtensionStatus) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// An object containing the file or file version's `id` (versionId) and `name`. +type BetaV2FileUploadResponseVersionInfo struct { + // Unique identifier of the file version. + ID string `json:"id"` + // Name of the file version. + Name string `json:"name"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ID respjson.Field + Name respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r BetaV2FileUploadResponseVersionInfo) RawJSON() string { return r.JSON.raw } +func (r *BetaV2FileUploadResponseVersionInfo) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type BetaV2FileUploadParams struct { + // The API accepts any of the following: + // + // - **Binary data** – send the raw bytes as `multipart/form-data`. + // - **HTTP / HTTPS URL** – a publicly reachable URL that ImageKit’s servers can + // fetch. + // - **Base64 string** – the file encoded as a Base64 data URI or plain Base64. + // + // When supplying a URL, the server must receive the response headers within 8 + // seconds; otherwise the request fails with 400 Bad Request. + File io.Reader `json:"file,omitzero,required" format:"binary"` + // The name with which the file has to be uploaded. + FileName string `json:"fileName,required"` + // This is the client-generated JSON Web Token (JWT). The ImageKit.io server uses + // it to authenticate and check that the upload request parameters have not been + // tampered with after the token has been generated. Learn how to create the token + // on the page below. This field is only required for authentication when uploading + // a file from the client side. + // + // **Note**: Sending a JWT that has been used in the past will result in a + // validation error. Even if your previous request resulted in an error, you should + // always send a new token. + // + // **⚠️Warning**: JWT must be generated on the server-side because it is generated + // using your account's private API key. This field is required for authentication + // when uploading a file from the client-side. + Token param.Opt[string] `json:"token,omitzero"` + // Server-side checks to run on the asset. Read more about + // [Upload API checks](/docs/api-reference/upload-file/upload-file-v2#upload-api-checks). + Checks param.Opt[string] `json:"checks,omitzero"` + // Define an important area in the image. This is only relevant for image type + // files. + // + // - To be passed as a string with the x and y coordinates of the top-left corner, + // and width and height of the area of interest in the format `x,y,width,height`. + // For example - `10,10,100,100` + // - Can be used with fo-customtransformation. + // - If this field is not specified and the file is overwritten, then + // customCoordinates will be removed. + CustomCoordinates param.Opt[string] `json:"customCoordinates,omitzero"` + // Optional text to describe the contents of the file. + Description param.Opt[string] `json:"description,omitzero"` + // The folder path in which the image has to be uploaded. If the folder(s) didn't + // exist before, a new folder(s) is created. Using multiple `/` creates a nested + // folder. + Folder param.Opt[string] `json:"folder,omitzero"` + // Whether to mark the file as private or not. + // + // If `true`, the file is marked as private and is accessible only using named + // transformation or signed URL. + IsPrivateFile param.Opt[bool] `json:"isPrivateFile,omitzero"` + // Whether to upload file as published or not. + // + // If `false`, the file is marked as unpublished, which restricts access to the + // file only via the media library. Files in draft or unpublished state can only be + // publicly accessed after being published. + // + // The option to upload in draft state is only available in custom enterprise + // pricing plans. + IsPublished param.Opt[bool] `json:"isPublished,omitzero"` + // If set to `true` and a file already exists at the exact location, its AITags + // will be removed. Set `overwriteAITags` to `false` to preserve AITags. + OverwriteAITags param.Opt[bool] `json:"overwriteAITags,omitzero"` + // If the request does not have `customMetadata`, and a file already exists at the + // exact location, existing customMetadata will be removed. + OverwriteCustomMetadata param.Opt[bool] `json:"overwriteCustomMetadata,omitzero"` + // If `false` and `useUniqueFileName` is also `false`, and a file already exists at + // the exact location, upload API will return an error immediately. + OverwriteFile param.Opt[bool] `json:"overwriteFile,omitzero"` + // If the request does not have `tags`, and a file already exists at the exact + // location, existing tags will be removed. + OverwriteTags param.Opt[bool] `json:"overwriteTags,omitzero"` + // Whether to use a unique filename for this file or not. + // + // If `true`, ImageKit.io will add a unique suffix to the filename parameter to get + // a unique filename. + // + // If `false`, then the image is uploaded with the provided filename parameter, and + // any existing file with the same name is replaced. + UseUniqueFileName param.Opt[bool] `json:"useUniqueFileName,omitzero"` + // The final status of extensions after they have completed execution will be + // delivered to this endpoint as a POST request. + // [Learn more](/docs/api-reference/digital-asset-management-dam/managing-assets/update-file-details#webhook-payload-structure) + // about the webhook payload structure. + WebhookURL param.Opt[string] `json:"webhookUrl,omitzero" format:"uri"` + // JSON key-value pairs to associate with the asset. Create the custom metadata + // fields before setting these values. + CustomMetadata map[string]any `json:"customMetadata,omitzero"` + // Array of extensions to be applied to the asset. Each extension can be configured + // with specific parameters based on the extension type. + Extensions shared.ExtensionsParam `json:"extensions,omitzero"` + // Array of response field keys to include in the API response body. + // + // Any of "tags", "customCoordinates", "isPrivateFile", "embeddedMetadata", + // "isPublished", "customMetadata", "metadata". + ResponseFields []string `json:"responseFields,omitzero"` + // Set the tags while uploading the file. Provide an array of tag strings (e.g. + // `["tag1", "tag2", "tag3"]`). The combined length of all tag characters must not + // exceed 500, and the `%` character is not allowed. If this field is not specified + // and the file is overwritten, the existing tags will be removed. + Tags []string `json:"tags,omitzero"` + // Configure pre-processing (`pre`) and post-processing (`post`) transformations. + // + // - `pre` — applied before the file is uploaded to the Media Library. + // Useful for reducing file size or applying basic optimizations upfront (e.g., + // resize, compress). + // + // - `post` — applied immediately after upload. + // Ideal for generating transformed versions (like video encodes or thumbnails) + // in advance, so they're ready for delivery without delay. + // + // You can mix and match any combination of post-processing types. + Transformation BetaV2FileUploadParamsTransformation `json:"transformation,omitzero"` + paramObj +} + +func (r BetaV2FileUploadParams) MarshalMultipart() (data []byte, contentType string, err error) { + buf := bytes.NewBuffer(nil) + writer := multipart.NewWriter(buf) + err = apiform.MarshalRoot(r, writer) + if err == nil { + err = apiform.WriteExtras(writer, r.ExtraFields()) + } + if err != nil { + writer.Close() + return nil, "", err + } + err = writer.Close() + if err != nil { + return nil, "", err + } + return buf.Bytes(), writer.FormDataContentType(), nil +} + +// Configure pre-processing (`pre`) and post-processing (`post`) transformations. +// +// - `pre` — applied before the file is uploaded to the Media Library. +// Useful for reducing file size or applying basic optimizations upfront (e.g., +// resize, compress). +// +// - `post` — applied immediately after upload. +// Ideal for generating transformed versions (like video encodes or thumbnails) +// in advance, so they're ready for delivery without delay. +// +// You can mix and match any combination of post-processing types. +type BetaV2FileUploadParamsTransformation struct { + // Transformation string to apply before uploading the file to the Media Library. + // Useful for optimizing files at ingestion. + Pre param.Opt[string] `json:"pre,omitzero"` + // List of transformations to apply _after_ the file is uploaded. + // Each item must match one of the following types: `transformation`, + // `gif-to-video`, `thumbnail`, `abs`. + Post []BetaV2FileUploadParamsTransformationPostUnion `json:"post,omitzero"` + paramObj +} + +func (r BetaV2FileUploadParamsTransformation) MarshalJSON() (data []byte, err error) { + type shadow BetaV2FileUploadParamsTransformation + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *BetaV2FileUploadParamsTransformation) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Only one field can be non-zero. +// +// Use [param.IsOmitted] to confirm if a field is set. +type BetaV2FileUploadParamsTransformationPostUnion struct { + OfTransformation *BetaV2FileUploadParamsTransformationPostTransformation `json:",omitzero,inline"` + OfGifToVideo *BetaV2FileUploadParamsTransformationPostGifToVideo `json:",omitzero,inline"` + OfThumbnail *BetaV2FileUploadParamsTransformationPostThumbnail `json:",omitzero,inline"` + OfAbs *BetaV2FileUploadParamsTransformationPostAbs `json:",omitzero,inline"` + paramUnion +} + +func (u BetaV2FileUploadParamsTransformationPostUnion) MarshalJSON() ([]byte, error) { + return param.MarshalUnion(u, u.OfTransformation, u.OfGifToVideo, u.OfThumbnail, u.OfAbs) +} +func (u *BetaV2FileUploadParamsTransformationPostUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, u) +} + +func (u *BetaV2FileUploadParamsTransformationPostUnion) asAny() any { + if !param.IsOmitted(u.OfTransformation) { + return u.OfTransformation + } else if !param.IsOmitted(u.OfGifToVideo) { + return u.OfGifToVideo + } else if !param.IsOmitted(u.OfThumbnail) { + return u.OfThumbnail + } else if !param.IsOmitted(u.OfAbs) { + return u.OfAbs + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u BetaV2FileUploadParamsTransformationPostUnion) GetProtocol() *string { + if vt := u.OfAbs; vt != nil { + return &vt.Protocol + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u BetaV2FileUploadParamsTransformationPostUnion) GetType() *string { + if vt := u.OfTransformation; vt != nil { + return (*string)(&vt.Type) + } else if vt := u.OfGifToVideo; vt != nil { + return (*string)(&vt.Type) + } else if vt := u.OfThumbnail; vt != nil { + return (*string)(&vt.Type) + } else if vt := u.OfAbs; vt != nil { + return (*string)(&vt.Type) + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u BetaV2FileUploadParamsTransformationPostUnion) GetValue() *string { + if vt := u.OfTransformation; vt != nil { + return (*string)(&vt.Value) + } else if vt := u.OfGifToVideo; vt != nil && vt.Value.Valid() { + return &vt.Value.Value + } else if vt := u.OfThumbnail; vt != nil && vt.Value.Valid() { + return &vt.Value.Value + } else if vt := u.OfAbs; vt != nil { + return (*string)(&vt.Value) + } + return nil +} + +func init() { + apijson.RegisterUnion[BetaV2FileUploadParamsTransformationPostUnion]( + "type", + apijson.Discriminator[BetaV2FileUploadParamsTransformationPostTransformation]("transformation"), + apijson.Discriminator[BetaV2FileUploadParamsTransformationPostGifToVideo]("gif-to-video"), + apijson.Discriminator[BetaV2FileUploadParamsTransformationPostThumbnail]("thumbnail"), + apijson.Discriminator[BetaV2FileUploadParamsTransformationPostAbs]("abs"), + ) +} + +// The properties Type, Value are required. +type BetaV2FileUploadParamsTransformationPostTransformation struct { + // Transformation string (e.g. `w-200,h-200`). + // Same syntax as ImageKit URL-based transformations. + Value string `json:"value,required"` + // Transformation type. + // + // This field can be elided, and will marshal its zero value as "transformation". + Type constant.Transformation `json:"type,required"` + paramObj +} + +func (r BetaV2FileUploadParamsTransformationPostTransformation) MarshalJSON() (data []byte, err error) { + type shadow BetaV2FileUploadParamsTransformationPostTransformation + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *BetaV2FileUploadParamsTransformationPostTransformation) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// The property Type is required. +type BetaV2FileUploadParamsTransformationPostGifToVideo struct { + // Optional transformation string to apply to the output video. + // **Example**: `q-80` + Value param.Opt[string] `json:"value,omitzero"` + // Converts an animated GIF into an MP4. + // + // This field can be elided, and will marshal its zero value as "gif-to-video". + Type constant.GifToVideo `json:"type,required"` + paramObj +} + +func (r BetaV2FileUploadParamsTransformationPostGifToVideo) MarshalJSON() (data []byte, err error) { + type shadow BetaV2FileUploadParamsTransformationPostGifToVideo + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *BetaV2FileUploadParamsTransformationPostGifToVideo) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// The property Type is required. +type BetaV2FileUploadParamsTransformationPostThumbnail struct { + // Optional transformation string. + // **Example**: `w-150,h-150` + Value param.Opt[string] `json:"value,omitzero"` + // Generates a thumbnail image. + // + // This field can be elided, and will marshal its zero value as "thumbnail". + Type constant.Thumbnail `json:"type,required"` + paramObj +} + +func (r BetaV2FileUploadParamsTransformationPostThumbnail) MarshalJSON() (data []byte, err error) { + type shadow BetaV2FileUploadParamsTransformationPostThumbnail + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *BetaV2FileUploadParamsTransformationPostThumbnail) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// The properties Protocol, Type, Value are required. +type BetaV2FileUploadParamsTransformationPostAbs struct { + // Streaming protocol to use (`hls` or `dash`). + // + // Any of "hls", "dash". + Protocol string `json:"protocol,omitzero,required"` + // List of different representations you want to create separated by an underscore. + Value string `json:"value,required"` + // Adaptive Bitrate Streaming (ABS) setup. + // + // This field can be elided, and will marshal its zero value as "abs". + Type constant.Abs `json:"type,required"` + paramObj +} + +func (r BetaV2FileUploadParamsTransformationPostAbs) MarshalJSON() (data []byte, err error) { + type shadow BetaV2FileUploadParamsTransformationPostAbs + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *BetaV2FileUploadParamsTransformationPostAbs) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +func init() { + apijson.RegisterFieldValidator[BetaV2FileUploadParamsTransformationPostAbs]( + "protocol", "hls", "dash", + ) +} diff --git a/betav2file_test.go b/betav2file_test.go new file mode 100644 index 0000000..b1d8780 --- /dev/null +++ b/betav2file_test.go @@ -0,0 +1,94 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit_test + +import ( + "bytes" + "context" + "errors" + "io" + "os" + "testing" + + "github.com/imagekit-developer/imagekit-go" + "github.com/imagekit-developer/imagekit-go/internal/testutil" + "github.com/imagekit-developer/imagekit-go/option" + "github.com/imagekit-developer/imagekit-go/shared" +) + +func TestBetaV2FileUploadWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Beta.V2.Files.Upload(context.TODO(), imagekit.BetaV2FileUploadParams{ + File: io.Reader(bytes.NewBuffer([]byte("some file contents"))), + FileName: "fileName", + Token: imagekit.String("token"), + Checks: imagekit.String("\"request.folder\" : \"marketing/\"\n"), + CustomCoordinates: imagekit.String("customCoordinates"), + CustomMetadata: map[string]any{ + "brand": "bar", + "color": "bar", + }, + Description: imagekit.String("Running shoes"), + Extensions: shared.ExtensionsParam{shared.ExtensionUnionParam{ + OfRemoveBg: &shared.ExtensionRemoveBgParam{ + Options: shared.ExtensionRemoveBgOptionsParam{ + AddShadow: imagekit.Bool(true), + BgColor: imagekit.String("bg_color"), + BgImageURL: imagekit.String("bg_image_url"), + Semitransparency: imagekit.Bool(true), + }, + }, + }, shared.ExtensionUnionParam{ + OfAutoTagging: &shared.ExtensionAutoTaggingParam{ + MaxTags: 5, + MinConfidence: 95, + Name: "google-auto-tagging", + }, + }, shared.ExtensionUnionParam{ + OfAIAutoDescription: &shared.ExtensionAIAutoDescriptionParam{}, + }}, + Folder: imagekit.String("folder"), + IsPrivateFile: imagekit.Bool(true), + IsPublished: imagekit.Bool(true), + OverwriteAITags: imagekit.Bool(true), + OverwriteCustomMetadata: imagekit.Bool(true), + OverwriteFile: imagekit.Bool(true), + OverwriteTags: imagekit.Bool(true), + ResponseFields: []string{"tags", "customCoordinates", "isPrivateFile"}, + Tags: []string{"t-shirt", "round-neck", "men"}, + Transformation: imagekit.BetaV2FileUploadParamsTransformation{ + Post: []imagekit.BetaV2FileUploadParamsTransformationPostUnion{{ + OfThumbnail: &imagekit.BetaV2FileUploadParamsTransformationPostThumbnail{ + Value: imagekit.String("w-150,h-150"), + }, + }, { + OfAbs: &imagekit.BetaV2FileUploadParamsTransformationPostAbs{ + Protocol: "dash", + Value: "sr-240_360_480_720_1080", + }, + }}, + Pre: imagekit.String("w-300,h-300,q-80"), + }, + UseUniqueFileName: imagekit.Bool(true), + WebhookURL: imagekit.String("https://example.com"), + }) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/cache.go b/cache.go new file mode 100644 index 0000000..51b3a0d --- /dev/null +++ b/cache.go @@ -0,0 +1,28 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit + +import ( + "github.com/imagekit-developer/imagekit-go/option" +) + +// CacheService contains methods and other services that help with interacting with +// the ImageKit API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewCacheService] method instead. +type CacheService struct { + Options []option.RequestOption + Invalidation CacheInvalidationService +} + +// NewCacheService generates a new service that applies the given options to each +// request. These options are applied after the parent client's options (if there +// is one), and before any request-specific options. +func NewCacheService(opts ...option.RequestOption) (r CacheService) { + r = CacheService{} + r.Options = opts + r.Invalidation = NewCacheInvalidationService(opts...) + return +} diff --git a/cacheinvalidation.go b/cacheinvalidation.go new file mode 100644 index 0000000..2088891 --- /dev/null +++ b/cacheinvalidation.go @@ -0,0 +1,116 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/imagekit-developer/imagekit-go/internal/apijson" + "github.com/imagekit-developer/imagekit-go/internal/requestconfig" + "github.com/imagekit-developer/imagekit-go/option" + "github.com/imagekit-developer/imagekit-go/packages/param" + "github.com/imagekit-developer/imagekit-go/packages/respjson" +) + +// CacheInvalidationService contains methods and other services that help with +// interacting with the ImageKit API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewCacheInvalidationService] method instead. +type CacheInvalidationService struct { + Options []option.RequestOption +} + +// NewCacheInvalidationService generates a new service that applies the given +// options to each request. These options are applied after the parent client's +// options (if there is one), and before any request-specific options. +func NewCacheInvalidationService(opts ...option.RequestOption) (r CacheInvalidationService) { + r = CacheInvalidationService{} + r.Options = opts + return +} + +// This API will purge CDN cache and ImageKit.io's internal cache for a file. Note: +// Purge cache is an asynchronous process and it may take some time to reflect the +// changes. +func (r *CacheInvalidationService) New(ctx context.Context, body CacheInvalidationNewParams, opts ...option.RequestOption) (res *CacheInvalidationNewResponse, err error) { + opts = append(r.Options[:], opts...) + path := "v1/files/purge" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +// This API returns the status of a purge cache request. +func (r *CacheInvalidationService) Get(ctx context.Context, requestID string, opts ...option.RequestOption) (res *CacheInvalidationGetResponse, err error) { + opts = append(r.Options[:], opts...) + if requestID == "" { + err = errors.New("missing required requestId parameter") + return + } + path := fmt.Sprintf("v1/files/purge/%s", requestID) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return +} + +type CacheInvalidationNewResponse struct { + // Unique identifier of the purge request. This can be used to check the status of + // the purge request. + RequestID string `json:"requestId"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + RequestID respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r CacheInvalidationNewResponse) RawJSON() string { return r.JSON.raw } +func (r *CacheInvalidationNewResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type CacheInvalidationGetResponse struct { + // Status of the purge request. + // + // Any of "Pending", "Completed". + Status CacheInvalidationGetResponseStatus `json:"status"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Status respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r CacheInvalidationGetResponse) RawJSON() string { return r.JSON.raw } +func (r *CacheInvalidationGetResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Status of the purge request. +type CacheInvalidationGetResponseStatus string + +const ( + CacheInvalidationGetResponseStatusPending CacheInvalidationGetResponseStatus = "Pending" + CacheInvalidationGetResponseStatusCompleted CacheInvalidationGetResponseStatus = "Completed" +) + +type CacheInvalidationNewParams struct { + // The full URL of the file to be purged. + URL string `json:"url,required" format:"uri"` + paramObj +} + +func (r CacheInvalidationNewParams) MarshalJSON() (data []byte, err error) { + type shadow CacheInvalidationNewParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *CacheInvalidationNewParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} diff --git a/cacheinvalidation_test.go b/cacheinvalidation_test.go new file mode 100644 index 0000000..c5f20b1 --- /dev/null +++ b/cacheinvalidation_test.go @@ -0,0 +1,64 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/imagekit-developer/imagekit-go" + "github.com/imagekit-developer/imagekit-go/internal/testutil" + "github.com/imagekit-developer/imagekit-go/option" +) + +func TestCacheInvalidationNew(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Cache.Invalidation.New(context.TODO(), imagekit.CacheInvalidationNewParams{ + URL: "https://ik.imagekit.io/your_imagekit_id/default-image.jpg", + }) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestCacheInvalidationGet(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Cache.Invalidation.Get(context.TODO(), "requestId") + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/client.go b/client.go new file mode 100644 index 0000000..6496787 --- /dev/null +++ b/client.go @@ -0,0 +1,139 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit + +import ( + "context" + "net/http" + "os" + + "github.com/imagekit-developer/imagekit-go/internal/requestconfig" + "github.com/imagekit-developer/imagekit-go/option" +) + +// Client creates a struct with services and top level methods that help with +// interacting with the ImageKit API. You should not instantiate this client +// directly, and instead use the [NewClient] method instead. +type Client struct { + Options []option.RequestOption + CustomMetadataFields CustomMetadataFieldService + Files FileService + Assets AssetService + Cache CacheService + Folders FolderService + Accounts AccountService + Beta BetaService + Webhooks WebhookService +} + +// DefaultClientOptions read from the environment (IMAGEKIT_PRIVATE_API_KEY, +// OPTIONAL_IMAGEKIT_IGNORES_THIS, IMAGEKIT_WEBHOOK_SECRET, IMAGE_KIT_BASE_URL). +// This should be used to initialize new clients. +func DefaultClientOptions() []option.RequestOption { + defaults := []option.RequestOption{option.WithEnvironmentProduction()} + if o, ok := os.LookupEnv("IMAGE_KIT_BASE_URL"); ok { + defaults = append(defaults, option.WithBaseURL(o)) + } + defaults = append(defaults, option.WithPassword("do_not_set")) + if o, ok := os.LookupEnv("IMAGEKIT_PRIVATE_API_KEY"); ok { + defaults = append(defaults, option.WithPrivateAPIKey(o)) + } + if o, ok := os.LookupEnv("OPTIONAL_IMAGEKIT_IGNORES_THIS"); ok { + defaults = append(defaults, option.WithPassword(o)) + } + if o, ok := os.LookupEnv("IMAGEKIT_WEBHOOK_SECRET"); ok { + defaults = append(defaults, option.WithWebhookSecret(o)) + } + return defaults +} + +// NewClient generates a new client with the default option read from the +// environment (IMAGEKIT_PRIVATE_API_KEY, OPTIONAL_IMAGEKIT_IGNORES_THIS, +// IMAGEKIT_WEBHOOK_SECRET, IMAGE_KIT_BASE_URL). The option passed in as arguments +// are applied after these default arguments, and all option will be passed down to +// the services and requests that this client makes. +func NewClient(opts ...option.RequestOption) (r Client) { + opts = append(DefaultClientOptions(), opts...) + + r = Client{Options: opts} + + r.CustomMetadataFields = NewCustomMetadataFieldService(opts...) + r.Files = NewFileService(opts...) + r.Assets = NewAssetService(opts...) + r.Cache = NewCacheService(opts...) + r.Folders = NewFolderService(opts...) + r.Accounts = NewAccountService(opts...) + r.Beta = NewBetaService(opts...) + r.Webhooks = NewWebhookService(opts...) + + return +} + +// Execute makes a request with the given context, method, URL, request params, +// response, and request options. This is useful for hitting undocumented endpoints +// while retaining the base URL, auth, retries, and other options from the client. +// +// If a byte slice or an [io.Reader] is supplied to params, it will be used as-is +// for the request body. +// +// The params is by default serialized into the body using [encoding/json]. If your +// type implements a MarshalJSON function, it will be used instead to serialize the +// request. If a URLQuery method is implemented, the returned [url.Values] will be +// used as query strings to the url. +// +// If your params struct uses [param.Field], you must provide either [MarshalJSON], +// [URLQuery], and/or [MarshalForm] functions. It is undefined behavior to use a +// struct uses [param.Field] without specifying how it is serialized. +// +// Any "…Params" object defined in this library can be used as the request +// argument. Note that 'path' arguments will not be forwarded into the url. +// +// The response body will be deserialized into the res variable, depending on its +// type: +// +// - A pointer to a [*http.Response] is populated by the raw response. +// - A pointer to a byte array will be populated with the contents of the request +// body. +// - A pointer to any other type uses this library's default JSON decoding, which +// respects UnmarshalJSON if it is defined on the type. +// - A nil value will not read the response body. +// +// For even greater flexibility, see [option.WithResponseInto] and +// [option.WithResponseBodyInto]. +func (r *Client) Execute(ctx context.Context, method string, path string, params any, res any, opts ...option.RequestOption) error { + opts = append(r.Options, opts...) + return requestconfig.ExecuteNewRequest(ctx, method, path, params, res, opts...) +} + +// Get makes a GET request with the given URL, params, and optionally deserializes +// to a response. See [Execute] documentation on the params and response. +func (r *Client) Get(ctx context.Context, path string, params any, res any, opts ...option.RequestOption) error { + return r.Execute(ctx, http.MethodGet, path, params, res, opts...) +} + +// Post makes a POST request with the given URL, params, and optionally +// deserializes to a response. See [Execute] documentation on the params and +// response. +func (r *Client) Post(ctx context.Context, path string, params any, res any, opts ...option.RequestOption) error { + return r.Execute(ctx, http.MethodPost, path, params, res, opts...) +} + +// Put makes a PUT request with the given URL, params, and optionally deserializes +// to a response. See [Execute] documentation on the params and response. +func (r *Client) Put(ctx context.Context, path string, params any, res any, opts ...option.RequestOption) error { + return r.Execute(ctx, http.MethodPut, path, params, res, opts...) +} + +// Patch makes a PATCH request with the given URL, params, and optionally +// deserializes to a response. See [Execute] documentation on the params and +// response. +func (r *Client) Patch(ctx context.Context, path string, params any, res any, opts ...option.RequestOption) error { + return r.Execute(ctx, http.MethodPatch, path, params, res, opts...) +} + +// Delete makes a DELETE request with the given URL, params, and optionally +// deserializes to a response. See [Execute] documentation on the params and +// response. +func (r *Client) Delete(ctx context.Context, path string, params any, res any, opts ...option.RequestOption) error { + return r.Execute(ctx, http.MethodDelete, path, params, res, opts...) +} diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..9202483 --- /dev/null +++ b/client_test.go @@ -0,0 +1,277 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit_test + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "reflect" + "testing" + "time" + + "github.com/imagekit-developer/imagekit-go" + "github.com/imagekit-developer/imagekit-go/internal" + "github.com/imagekit-developer/imagekit-go/option" +) + +type closureTransport struct { + fn func(req *http.Request) (*http.Response, error) +} + +func (t *closureTransport) RoundTrip(req *http.Request) (*http.Response, error) { + return t.fn(req) +} + +func TestUserAgentHeader(t *testing.T) { + var userAgent string + client := imagekit.NewClient( + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + userAgent = req.Header.Get("User-Agent") + return &http.Response{ + StatusCode: http.StatusOK, + }, nil + }, + }, + }), + ) + client.Files.Upload(context.Background(), imagekit.FileUploadParams{ + File: io.Reader(bytes.NewBuffer([]byte("https://www.example.com/public-url.jpg"))), + FileName: "file-name.jpg", + }) + if userAgent != fmt.Sprintf("ImageKit/Go %s", internal.PackageVersion) { + t.Errorf("Expected User-Agent to be correct, but got: %#v", userAgent) + } +} + +func TestRetryAfter(t *testing.T) { + retryCountHeaders := make([]string, 0) + client := imagekit.NewClient( + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + retryCountHeaders = append(retryCountHeaders, req.Header.Get("X-Stainless-Retry-Count")) + return &http.Response{ + StatusCode: http.StatusTooManyRequests, + Header: http.Header{ + http.CanonicalHeaderKey("Retry-After"): []string{"0.1"}, + }, + }, nil + }, + }, + }), + ) + _, err := client.Files.Upload(context.Background(), imagekit.FileUploadParams{ + File: io.Reader(bytes.NewBuffer([]byte("https://www.example.com/public-url.jpg"))), + FileName: "file-name.jpg", + }) + if err == nil { + t.Error("Expected there to be a cancel error") + } + + attempts := len(retryCountHeaders) + if attempts != 3 { + t.Errorf("Expected %d attempts, got %d", 3, attempts) + } + + expectedRetryCountHeaders := []string{"0", "1", "2"} + if !reflect.DeepEqual(retryCountHeaders, expectedRetryCountHeaders) { + t.Errorf("Expected %v retry count headers, got %v", expectedRetryCountHeaders, retryCountHeaders) + } +} + +func TestDeleteRetryCountHeader(t *testing.T) { + retryCountHeaders := make([]string, 0) + client := imagekit.NewClient( + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + retryCountHeaders = append(retryCountHeaders, req.Header.Get("X-Stainless-Retry-Count")) + return &http.Response{ + StatusCode: http.StatusTooManyRequests, + Header: http.Header{ + http.CanonicalHeaderKey("Retry-After"): []string{"0.1"}, + }, + }, nil + }, + }, + }), + option.WithHeaderDel("X-Stainless-Retry-Count"), + ) + _, err := client.Files.Upload(context.Background(), imagekit.FileUploadParams{ + File: io.Reader(bytes.NewBuffer([]byte("https://www.example.com/public-url.jpg"))), + FileName: "file-name.jpg", + }) + if err == nil { + t.Error("Expected there to be a cancel error") + } + + expectedRetryCountHeaders := []string{"", "", ""} + if !reflect.DeepEqual(retryCountHeaders, expectedRetryCountHeaders) { + t.Errorf("Expected %v retry count headers, got %v", expectedRetryCountHeaders, retryCountHeaders) + } +} + +func TestOverwriteRetryCountHeader(t *testing.T) { + retryCountHeaders := make([]string, 0) + client := imagekit.NewClient( + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + retryCountHeaders = append(retryCountHeaders, req.Header.Get("X-Stainless-Retry-Count")) + return &http.Response{ + StatusCode: http.StatusTooManyRequests, + Header: http.Header{ + http.CanonicalHeaderKey("Retry-After"): []string{"0.1"}, + }, + }, nil + }, + }, + }), + option.WithHeader("X-Stainless-Retry-Count", "42"), + ) + _, err := client.Files.Upload(context.Background(), imagekit.FileUploadParams{ + File: io.Reader(bytes.NewBuffer([]byte("https://www.example.com/public-url.jpg"))), + FileName: "file-name.jpg", + }) + if err == nil { + t.Error("Expected there to be a cancel error") + } + + expectedRetryCountHeaders := []string{"42", "42", "42"} + if !reflect.DeepEqual(retryCountHeaders, expectedRetryCountHeaders) { + t.Errorf("Expected %v retry count headers, got %v", expectedRetryCountHeaders, retryCountHeaders) + } +} + +func TestRetryAfterMs(t *testing.T) { + attempts := 0 + client := imagekit.NewClient( + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + attempts++ + return &http.Response{ + StatusCode: http.StatusTooManyRequests, + Header: http.Header{ + http.CanonicalHeaderKey("Retry-After-Ms"): []string{"100"}, + }, + }, nil + }, + }, + }), + ) + _, err := client.Files.Upload(context.Background(), imagekit.FileUploadParams{ + File: io.Reader(bytes.NewBuffer([]byte("https://www.example.com/public-url.jpg"))), + FileName: "file-name.jpg", + }) + if err == nil { + t.Error("Expected there to be a cancel error") + } + if want := 3; attempts != want { + t.Errorf("Expected %d attempts, got %d", want, attempts) + } +} + +func TestContextCancel(t *testing.T) { + client := imagekit.NewClient( + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + <-req.Context().Done() + return nil, req.Context().Err() + }, + }, + }), + ) + cancelCtx, cancel := context.WithCancel(context.Background()) + cancel() + _, err := client.Files.Upload(cancelCtx, imagekit.FileUploadParams{ + File: io.Reader(bytes.NewBuffer([]byte("https://www.example.com/public-url.jpg"))), + FileName: "file-name.jpg", + }) + if err == nil { + t.Error("Expected there to be a cancel error") + } +} + +func TestContextCancelDelay(t *testing.T) { + client := imagekit.NewClient( + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + <-req.Context().Done() + return nil, req.Context().Err() + }, + }, + }), + ) + cancelCtx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond) + defer cancel() + _, err := client.Files.Upload(cancelCtx, imagekit.FileUploadParams{ + File: io.Reader(bytes.NewBuffer([]byte("https://www.example.com/public-url.jpg"))), + FileName: "file-name.jpg", + }) + if err == nil { + t.Error("expected there to be a cancel error") + } +} + +func TestContextDeadline(t *testing.T) { + testTimeout := time.After(3 * time.Second) + testDone := make(chan struct{}) + + deadline := time.Now().Add(100 * time.Millisecond) + deadlineCtx, cancel := context.WithDeadline(context.Background(), deadline) + defer cancel() + + go func() { + client := imagekit.NewClient( + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + <-req.Context().Done() + return nil, req.Context().Err() + }, + }, + }), + ) + _, err := client.Files.Upload(deadlineCtx, imagekit.FileUploadParams{ + File: io.Reader(bytes.NewBuffer([]byte("https://www.example.com/public-url.jpg"))), + FileName: "file-name.jpg", + }) + if err == nil { + t.Error("expected there to be a deadline error") + } + close(testDone) + }() + + select { + case <-testTimeout: + t.Fatal("client didn't finish in time") + case <-testDone: + if diff := time.Since(deadline); diff < -30*time.Millisecond || 30*time.Millisecond < diff { + t.Fatalf("client did not return within 30ms of context deadline, got %s", diff) + } + } +} diff --git a/config/api.go b/config/api.go deleted file mode 100644 index 91f8cd7..0000000 --- a/config/api.go +++ /dev/null @@ -1,9 +0,0 @@ -package config - -// API defines the configuration for making requests to the ImageKit.io API. -type API struct { - Prefix string `default:"https://api.imagekit.io/v1/"` - UploadPrefix string `default:"https://upload.imagekit.io/api/v1/"` - Timeout int64 `default:"60"` // seconds - UploadTimeout int64 `upload_timeout"` -} diff --git a/config/cloud.go b/config/cloud.go deleted file mode 100644 index 6aee029..0000000 --- a/config/cloud.go +++ /dev/null @@ -1,8 +0,0 @@ -package config - -// Cloud defines the cloud configuration required to connect your application to ImageKit.io. -type Cloud struct { - PrivateKey string - PublicKey string - UrlEndpoint string -} diff --git a/config/configuration.go b/config/configuration.go deleted file mode 100644 index c91358c..0000000 --- a/config/configuration.go +++ /dev/null @@ -1,50 +0,0 @@ -// Package config defines the ImageKit configuration. -package config - -import ( - "errors" - "os" - - "github.com/creasty/defaults" -) - -// Configuration is the main configuration struct. -type Configuration struct { - API API - Cloud Cloud -} - -// New returns a new Configuration instance from the environment variables -func New() (*Configuration, error) { - privateKey := os.Getenv("IMAGEKIT_PRIVATE_KEY") - publicKey := os.Getenv("IMAGEKIT_PUBLIC_KEY") - endpointUrl := os.Getenv("IMAGEKIT_ENDPOINT_URL") - - switch { - case privateKey == "": - return nil, errors.New("IMAGEKIT_PRIVATE_KEY envvar not set") - case publicKey == "": - return nil, errors.New("IMAGEKIT_PUBLIC_KEY envvar not set") - case endpointUrl == "": - return nil, errors.New("IMAGEKIT_ENDPOINT_URL envvar not set") - } - - return NewFromParams(privateKey, publicKey, endpointUrl), nil -} - -// NewFromParams returns a new Configuration instance from the provided keys and endpointUrl. -func NewFromParams(privateKey string, publicKey string, endpointUrl string) *Configuration { - cloudConf := Cloud{ - PrivateKey: privateKey, - PublicKey: publicKey, - UrlEndpoint: endpointUrl, - } - - var api = API{} - defaults.Set(&api) - - return &Configuration{ - Cloud: cloudConf, - API: api, - } -} diff --git a/config/configuration_test.go b/config/configuration_test.go deleted file mode 100644 index f19d7b7..0000000 --- a/config/configuration_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package config_test - -import ( - "os" - "testing" - - "github.com/imagekit-developer/imagekit-go/config" - "github.com/stretchr/testify/assert" -) - -func TestConfiguration_CreateInstance(t *testing.T) { - os.Unsetenv("IMAGEKIT_PRIVATE_KEY") - os.Unsetenv("IMAGEKIT_PUBLIC_KEY") - os.Unsetenv("IMAGEKIT_ENDPOINT_URL") - - defer os.Unsetenv("IMAGEKIT_PRIVATE_KEY") - defer os.Unsetenv("IMAGEKIT_PUBLIC_KEY") - defer os.Unsetenv("IMAGEKIT_ENDPOINT_URL") - - c, err := config.New() - - if err == nil { - t.Error("expected error") - } - - os.Setenv("IMAGEKIT_PRIVATE_KEY", "private_") - - c, err = config.New() - - if err == nil { - t.Error("expected error") - } - - os.Setenv("IMAGEKIT_PUBLIC_KEY", "public_") - c, err = config.New() - - if err == nil { - t.Error("expected error") - } - - os.Setenv("IMAGEKIT_PUBLIC_KEY", "public_") - c, err = config.New() - - if err == nil { - t.Error("expected error") - } - - os.Setenv("IMAGEKIT_ENDPOINT_URL", "https://ik.imagekit.io/test/") - - c, err = config.New() - - if err != nil { - t.Error("Unexpected error") - } - - c = config.NewFromParams("private", "public", "https://example/nature") - - assert.Equal(t, "private", c.Cloud.PrivateKey) - assert.Equal(t, "public", c.Cloud.PublicKey) -} diff --git a/custommetadatafield.go b/custommetadatafield.go new file mode 100644 index 0000000..47c2246 --- /dev/null +++ b/custommetadatafield.go @@ -0,0 +1,811 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + + "github.com/imagekit-developer/imagekit-go/internal/apijson" + "github.com/imagekit-developer/imagekit-go/internal/apiquery" + "github.com/imagekit-developer/imagekit-go/internal/requestconfig" + "github.com/imagekit-developer/imagekit-go/option" + "github.com/imagekit-developer/imagekit-go/packages/param" + "github.com/imagekit-developer/imagekit-go/packages/respjson" +) + +// CustomMetadataFieldService contains methods and other services that help with +// interacting with the ImageKit API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewCustomMetadataFieldService] method instead. +type CustomMetadataFieldService struct { + Options []option.RequestOption +} + +// NewCustomMetadataFieldService generates a new service that applies the given +// options to each request. These options are applied after the parent client's +// options (if there is one), and before any request-specific options. +func NewCustomMetadataFieldService(opts ...option.RequestOption) (r CustomMetadataFieldService) { + r = CustomMetadataFieldService{} + r.Options = opts + return +} + +// This API creates a new custom metadata field. Once a custom metadata field is +// created either through this API or using the dashboard UI, its value can be set +// on the assets. The value of a field for an asset can be set using the media +// library UI or programmatically through upload or update assets API. +func (r *CustomMetadataFieldService) New(ctx context.Context, body CustomMetadataFieldNewParams, opts ...option.RequestOption) (res *CustomMetadataField, err error) { + opts = append(r.Options[:], opts...) + path := "v1/customMetadataFields" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +// This API updates the label or schema of an existing custom metadata field. +func (r *CustomMetadataFieldService) Update(ctx context.Context, id string, body CustomMetadataFieldUpdateParams, opts ...option.RequestOption) (res *CustomMetadataField, err error) { + opts = append(r.Options[:], opts...) + if id == "" { + err = errors.New("missing required id parameter") + return + } + path := fmt.Sprintf("v1/customMetadataFields/%s", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPatch, path, body, &res, opts...) + return +} + +// This API returns the array of created custom metadata field objects. By default +// the API returns only non deleted field objects, but you can include deleted +// fields in the API response. +func (r *CustomMetadataFieldService) List(ctx context.Context, query CustomMetadataFieldListParams, opts ...option.RequestOption) (res *[]CustomMetadataField, err error) { + opts = append(r.Options[:], opts...) + path := "v1/customMetadataFields" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) + return +} + +// This API deletes a custom metadata field. Even after deleting a custom metadata +// field, you cannot create any new custom metadata field with the same name. +func (r *CustomMetadataFieldService) Delete(ctx context.Context, id string, opts ...option.RequestOption) (res *CustomMetadataFieldDeleteResponse, err error) { + opts = append(r.Options[:], opts...) + if id == "" { + err = errors.New("missing required id parameter") + return + } + path := fmt.Sprintf("v1/customMetadataFields/%s", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, &res, opts...) + return +} + +// Object containing details of a custom metadata field. +type CustomMetadataField struct { + // Unique identifier for the custom metadata field. Use this to update the field. + ID string `json:"id,required"` + // Human readable name of the custom metadata field. This name is displayed as form + // field label to the users while setting field value on the asset in the media + // library UI. + Label string `json:"label,required"` + // API name of the custom metadata field. This becomes the key while setting + // `customMetadata` (key-value object) for an asset using upload or update API. + Name string `json:"name,required"` + // An object that describes the rules for the custom metadata field value. + Schema CustomMetadataFieldSchema `json:"schema,required"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ID respjson.Field + Label respjson.Field + Name respjson.Field + Schema respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r CustomMetadataField) RawJSON() string { return r.JSON.raw } +func (r *CustomMetadataField) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// An object that describes the rules for the custom metadata field value. +type CustomMetadataFieldSchema struct { + // Type of the custom metadata field. + // + // Any of "Text", "Textarea", "Number", "Date", "Boolean", "SingleSelect", + // "MultiSelect". + Type string `json:"type,required"` + // The default value for this custom metadata field. Date type of default value + // depends on the field type. + DefaultValue CustomMetadataFieldSchemaDefaultValueUnion `json:"defaultValue"` + // Specifies if the this custom metadata field is required or not. + IsValueRequired bool `json:"isValueRequired"` + // Maximum length of string. Only set if `type` is set to `Text` or `Textarea`. + MaxLength float64 `json:"maxLength"` + // Maximum value of the field. Only set if field type is `Date` or `Number`. For + // `Date` type field, the value will be in ISO8601 string format. For `Number` type + // field, it will be a numeric value. + MaxValue CustomMetadataFieldSchemaMaxValueUnion `json:"maxValue"` + // Minimum length of string. Only set if `type` is set to `Text` or `Textarea`. + MinLength float64 `json:"minLength"` + // Minimum value of the field. Only set if field type is `Date` or `Number`. For + // `Date` type field, the value will be in ISO8601 string format. For `Number` type + // field, it will be a numeric value. + MinValue CustomMetadataFieldSchemaMinValueUnion `json:"minValue"` + // An array of allowed values when field type is `SingleSelect` or `MultiSelect`. + SelectOptions []CustomMetadataFieldSchemaSelectOptionUnion `json:"selectOptions"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Type respjson.Field + DefaultValue respjson.Field + IsValueRequired respjson.Field + MaxLength respjson.Field + MaxValue respjson.Field + MinLength respjson.Field + MinValue respjson.Field + SelectOptions respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r CustomMetadataFieldSchema) RawJSON() string { return r.JSON.raw } +func (r *CustomMetadataFieldSchema) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// CustomMetadataFieldSchemaDefaultValueUnion contains all possible properties and +// values from [string], [float64], [bool], +// [[]CustomMetadataFieldSchemaDefaultValueMixedItemUnion]. +// +// Use the methods beginning with 'As' to cast the union to one of its variants. +// +// If the underlying value is not a json object, one of the following properties +// will be valid: OfString OfFloat OfBool OfMixed] +type CustomMetadataFieldSchemaDefaultValueUnion struct { + // This field will be present if the value is a [string] instead of an object. + OfString string `json:",inline"` + // This field will be present if the value is a [float64] instead of an object. + OfFloat float64 `json:",inline"` + // This field will be present if the value is a [bool] instead of an object. + OfBool bool `json:",inline"` + // This field will be present if the value is a + // [[]CustomMetadataFieldSchemaDefaultValueMixedItemUnion] instead of an object. + OfMixed []CustomMetadataFieldSchemaDefaultValueMixedItemUnion `json:",inline"` + JSON struct { + OfString respjson.Field + OfFloat respjson.Field + OfBool respjson.Field + OfMixed respjson.Field + raw string + } `json:"-"` +} + +func (u CustomMetadataFieldSchemaDefaultValueUnion) AsString() (v string) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u CustomMetadataFieldSchemaDefaultValueUnion) AsFloat() (v float64) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u CustomMetadataFieldSchemaDefaultValueUnion) AsBool() (v bool) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u CustomMetadataFieldSchemaDefaultValueUnion) AsMixed() (v []CustomMetadataFieldSchemaDefaultValueMixedItemUnion) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +// Returns the unmodified JSON received from the API +func (u CustomMetadataFieldSchemaDefaultValueUnion) RawJSON() string { return u.JSON.raw } + +func (r *CustomMetadataFieldSchemaDefaultValueUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// CustomMetadataFieldSchemaDefaultValueMixedItemUnion contains all possible +// properties and values from [string], [float64], [bool]. +// +// Use the methods beginning with 'As' to cast the union to one of its variants. +// +// If the underlying value is not a json object, one of the following properties +// will be valid: OfString OfFloat OfBool] +type CustomMetadataFieldSchemaDefaultValueMixedItemUnion struct { + // This field will be present if the value is a [string] instead of an object. + OfString string `json:",inline"` + // This field will be present if the value is a [float64] instead of an object. + OfFloat float64 `json:",inline"` + // This field will be present if the value is a [bool] instead of an object. + OfBool bool `json:",inline"` + JSON struct { + OfString respjson.Field + OfFloat respjson.Field + OfBool respjson.Field + raw string + } `json:"-"` +} + +func (u CustomMetadataFieldSchemaDefaultValueMixedItemUnion) AsString() (v string) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u CustomMetadataFieldSchemaDefaultValueMixedItemUnion) AsFloat() (v float64) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u CustomMetadataFieldSchemaDefaultValueMixedItemUnion) AsBool() (v bool) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +// Returns the unmodified JSON received from the API +func (u CustomMetadataFieldSchemaDefaultValueMixedItemUnion) RawJSON() string { return u.JSON.raw } + +func (r *CustomMetadataFieldSchemaDefaultValueMixedItemUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// CustomMetadataFieldSchemaMaxValueUnion contains all possible properties and +// values from [string], [float64]. +// +// Use the methods beginning with 'As' to cast the union to one of its variants. +// +// If the underlying value is not a json object, one of the following properties +// will be valid: OfString OfFloat] +type CustomMetadataFieldSchemaMaxValueUnion struct { + // This field will be present if the value is a [string] instead of an object. + OfString string `json:",inline"` + // This field will be present if the value is a [float64] instead of an object. + OfFloat float64 `json:",inline"` + JSON struct { + OfString respjson.Field + OfFloat respjson.Field + raw string + } `json:"-"` +} + +func (u CustomMetadataFieldSchemaMaxValueUnion) AsString() (v string) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u CustomMetadataFieldSchemaMaxValueUnion) AsFloat() (v float64) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +// Returns the unmodified JSON received from the API +func (u CustomMetadataFieldSchemaMaxValueUnion) RawJSON() string { return u.JSON.raw } + +func (r *CustomMetadataFieldSchemaMaxValueUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// CustomMetadataFieldSchemaMinValueUnion contains all possible properties and +// values from [string], [float64]. +// +// Use the methods beginning with 'As' to cast the union to one of its variants. +// +// If the underlying value is not a json object, one of the following properties +// will be valid: OfString OfFloat] +type CustomMetadataFieldSchemaMinValueUnion struct { + // This field will be present if the value is a [string] instead of an object. + OfString string `json:",inline"` + // This field will be present if the value is a [float64] instead of an object. + OfFloat float64 `json:",inline"` + JSON struct { + OfString respjson.Field + OfFloat respjson.Field + raw string + } `json:"-"` +} + +func (u CustomMetadataFieldSchemaMinValueUnion) AsString() (v string) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u CustomMetadataFieldSchemaMinValueUnion) AsFloat() (v float64) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +// Returns the unmodified JSON received from the API +func (u CustomMetadataFieldSchemaMinValueUnion) RawJSON() string { return u.JSON.raw } + +func (r *CustomMetadataFieldSchemaMinValueUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// CustomMetadataFieldSchemaSelectOptionUnion contains all possible properties and +// values from [string], [float64], [bool]. +// +// Use the methods beginning with 'As' to cast the union to one of its variants. +// +// If the underlying value is not a json object, one of the following properties +// will be valid: OfString OfFloat OfBool] +type CustomMetadataFieldSchemaSelectOptionUnion struct { + // This field will be present if the value is a [string] instead of an object. + OfString string `json:",inline"` + // This field will be present if the value is a [float64] instead of an object. + OfFloat float64 `json:",inline"` + // This field will be present if the value is a [bool] instead of an object. + OfBool bool `json:",inline"` + JSON struct { + OfString respjson.Field + OfFloat respjson.Field + OfBool respjson.Field + raw string + } `json:"-"` +} + +func (u CustomMetadataFieldSchemaSelectOptionUnion) AsString() (v string) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u CustomMetadataFieldSchemaSelectOptionUnion) AsFloat() (v float64) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u CustomMetadataFieldSchemaSelectOptionUnion) AsBool() (v bool) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +// Returns the unmodified JSON received from the API +func (u CustomMetadataFieldSchemaSelectOptionUnion) RawJSON() string { return u.JSON.raw } + +func (r *CustomMetadataFieldSchemaSelectOptionUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type CustomMetadataFieldDeleteResponse struct { + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r CustomMetadataFieldDeleteResponse) RawJSON() string { return r.JSON.raw } +func (r *CustomMetadataFieldDeleteResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type CustomMetadataFieldNewParams struct { + // Human readable name of the custom metadata field. This should be unique across + // all non deleted custom metadata fields. This name is displayed as form field + // label to the users while setting field value on an asset in the media library + // UI. + Label string `json:"label,required"` + // API name of the custom metadata field. This should be unique across all + // (including deleted) custom metadata fields. + Name string `json:"name,required"` + Schema CustomMetadataFieldNewParamsSchema `json:"schema,omitzero,required"` + paramObj +} + +func (r CustomMetadataFieldNewParams) MarshalJSON() (data []byte, err error) { + type shadow CustomMetadataFieldNewParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *CustomMetadataFieldNewParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// The property Type is required. +type CustomMetadataFieldNewParamsSchema struct { + // Type of the custom metadata field. + // + // Any of "Text", "Textarea", "Number", "Date", "Boolean", "SingleSelect", + // "MultiSelect". + Type string `json:"type,omitzero,required"` + // Sets this custom metadata field as required. Setting custom metadata fields on + // an asset will throw error if the value for all required fields are not present + // in upload or update asset API request body. + IsValueRequired param.Opt[bool] `json:"isValueRequired,omitzero"` + // Maximum length of string. Only set this property if `type` is set to `Text` or + // `Textarea`. + MaxLength param.Opt[float64] `json:"maxLength,omitzero"` + // Minimum length of string. Only set this property if `type` is set to `Text` or + // `Textarea`. + MinLength param.Opt[float64] `json:"minLength,omitzero"` + // The default value for this custom metadata field. This property is only required + // if `isValueRequired` property is set to `true`. The value should match the + // `type` of custom metadata field. + DefaultValue CustomMetadataFieldNewParamsSchemaDefaultValueUnion `json:"defaultValue,omitzero"` + // Maximum value of the field. Only set this property if field type is `Date` or + // `Number`. For `Date` type field, set the minimum date in ISO8601 string format. + // For `Number` type field, set the minimum numeric value. + MaxValue CustomMetadataFieldNewParamsSchemaMaxValueUnion `json:"maxValue,omitzero"` + // Minimum value of the field. Only set this property if field type is `Date` or + // `Number`. For `Date` type field, set the minimum date in ISO8601 string format. + // For `Number` type field, set the minimum numeric value. + MinValue CustomMetadataFieldNewParamsSchemaMinValueUnion `json:"minValue,omitzero"` + // An array of allowed values. This property is only required if `type` property is + // set to `SingleSelect` or `MultiSelect`. + SelectOptions []CustomMetadataFieldNewParamsSchemaSelectOptionUnion `json:"selectOptions,omitzero"` + paramObj +} + +func (r CustomMetadataFieldNewParamsSchema) MarshalJSON() (data []byte, err error) { + type shadow CustomMetadataFieldNewParamsSchema + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *CustomMetadataFieldNewParamsSchema) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +func init() { + apijson.RegisterFieldValidator[CustomMetadataFieldNewParamsSchema]( + "type", "Text", "Textarea", "Number", "Date", "Boolean", "SingleSelect", "MultiSelect", + ) +} + +// Only one field can be non-zero. +// +// Use [param.IsOmitted] to confirm if a field is set. +type CustomMetadataFieldNewParamsSchemaDefaultValueUnion struct { + OfString param.Opt[string] `json:",omitzero,inline"` + OfFloat param.Opt[float64] `json:",omitzero,inline"` + OfBool param.Opt[bool] `json:",omitzero,inline"` + OfMixed []CustomMetadataFieldNewParamsSchemaDefaultValueMixedItemUnion `json:",omitzero,inline"` + paramUnion +} + +func (u CustomMetadataFieldNewParamsSchemaDefaultValueUnion) MarshalJSON() ([]byte, error) { + return param.MarshalUnion(u, u.OfString, u.OfFloat, u.OfBool, u.OfMixed) +} +func (u *CustomMetadataFieldNewParamsSchemaDefaultValueUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, u) +} + +func (u *CustomMetadataFieldNewParamsSchemaDefaultValueUnion) asAny() any { + if !param.IsOmitted(u.OfString) { + return &u.OfString.Value + } else if !param.IsOmitted(u.OfFloat) { + return &u.OfFloat.Value + } else if !param.IsOmitted(u.OfBool) { + return &u.OfBool.Value + } else if !param.IsOmitted(u.OfMixed) { + return &u.OfMixed + } + return nil +} + +// Only one field can be non-zero. +// +// Use [param.IsOmitted] to confirm if a field is set. +type CustomMetadataFieldNewParamsSchemaDefaultValueMixedItemUnion struct { + OfString param.Opt[string] `json:",omitzero,inline"` + OfFloat param.Opt[float64] `json:",omitzero,inline"` + OfBool param.Opt[bool] `json:",omitzero,inline"` + paramUnion +} + +func (u CustomMetadataFieldNewParamsSchemaDefaultValueMixedItemUnion) MarshalJSON() ([]byte, error) { + return param.MarshalUnion(u, u.OfString, u.OfFloat, u.OfBool) +} +func (u *CustomMetadataFieldNewParamsSchemaDefaultValueMixedItemUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, u) +} + +func (u *CustomMetadataFieldNewParamsSchemaDefaultValueMixedItemUnion) asAny() any { + if !param.IsOmitted(u.OfString) { + return &u.OfString.Value + } else if !param.IsOmitted(u.OfFloat) { + return &u.OfFloat.Value + } else if !param.IsOmitted(u.OfBool) { + return &u.OfBool.Value + } + return nil +} + +// Only one field can be non-zero. +// +// Use [param.IsOmitted] to confirm if a field is set. +type CustomMetadataFieldNewParamsSchemaMaxValueUnion struct { + OfString param.Opt[string] `json:",omitzero,inline"` + OfFloat param.Opt[float64] `json:",omitzero,inline"` + paramUnion +} + +func (u CustomMetadataFieldNewParamsSchemaMaxValueUnion) MarshalJSON() ([]byte, error) { + return param.MarshalUnion(u, u.OfString, u.OfFloat) +} +func (u *CustomMetadataFieldNewParamsSchemaMaxValueUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, u) +} + +func (u *CustomMetadataFieldNewParamsSchemaMaxValueUnion) asAny() any { + if !param.IsOmitted(u.OfString) { + return &u.OfString.Value + } else if !param.IsOmitted(u.OfFloat) { + return &u.OfFloat.Value + } + return nil +} + +// Only one field can be non-zero. +// +// Use [param.IsOmitted] to confirm if a field is set. +type CustomMetadataFieldNewParamsSchemaMinValueUnion struct { + OfString param.Opt[string] `json:",omitzero,inline"` + OfFloat param.Opt[float64] `json:",omitzero,inline"` + paramUnion +} + +func (u CustomMetadataFieldNewParamsSchemaMinValueUnion) MarshalJSON() ([]byte, error) { + return param.MarshalUnion(u, u.OfString, u.OfFloat) +} +func (u *CustomMetadataFieldNewParamsSchemaMinValueUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, u) +} + +func (u *CustomMetadataFieldNewParamsSchemaMinValueUnion) asAny() any { + if !param.IsOmitted(u.OfString) { + return &u.OfString.Value + } else if !param.IsOmitted(u.OfFloat) { + return &u.OfFloat.Value + } + return nil +} + +// Only one field can be non-zero. +// +// Use [param.IsOmitted] to confirm if a field is set. +type CustomMetadataFieldNewParamsSchemaSelectOptionUnion struct { + OfString param.Opt[string] `json:",omitzero,inline"` + OfFloat param.Opt[float64] `json:",omitzero,inline"` + OfBool param.Opt[bool] `json:",omitzero,inline"` + paramUnion +} + +func (u CustomMetadataFieldNewParamsSchemaSelectOptionUnion) MarshalJSON() ([]byte, error) { + return param.MarshalUnion(u, u.OfString, u.OfFloat, u.OfBool) +} +func (u *CustomMetadataFieldNewParamsSchemaSelectOptionUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, u) +} + +func (u *CustomMetadataFieldNewParamsSchemaSelectOptionUnion) asAny() any { + if !param.IsOmitted(u.OfString) { + return &u.OfString.Value + } else if !param.IsOmitted(u.OfFloat) { + return &u.OfFloat.Value + } else if !param.IsOmitted(u.OfBool) { + return &u.OfBool.Value + } + return nil +} + +type CustomMetadataFieldUpdateParams struct { + // Human readable name of the custom metadata field. This should be unique across + // all non deleted custom metadata fields. This name is displayed as form field + // label to the users while setting field value on an asset in the media library + // UI. This parameter is required if `schema` is not provided. + Label param.Opt[string] `json:"label,omitzero"` + // An object that describes the rules for the custom metadata key. This parameter + // is required if `label` is not provided. Note: `type` cannot be updated and will + // be ignored if sent with the `schema`. The schema will be validated as per the + // existing `type`. + Schema CustomMetadataFieldUpdateParamsSchema `json:"schema,omitzero"` + paramObj +} + +func (r CustomMetadataFieldUpdateParams) MarshalJSON() (data []byte, err error) { + type shadow CustomMetadataFieldUpdateParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *CustomMetadataFieldUpdateParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// An object that describes the rules for the custom metadata key. This parameter +// is required if `label` is not provided. Note: `type` cannot be updated and will +// be ignored if sent with the `schema`. The schema will be validated as per the +// existing `type`. +type CustomMetadataFieldUpdateParamsSchema struct { + // Sets this custom metadata field as required. Setting custom metadata fields on + // an asset will throw error if the value for all required fields are not present + // in upload or update asset API request body. + IsValueRequired param.Opt[bool] `json:"isValueRequired,omitzero"` + // Maximum length of string. Only set this property if `type` is set to `Text` or + // `Textarea`. + MaxLength param.Opt[float64] `json:"maxLength,omitzero"` + // Minimum length of string. Only set this property if `type` is set to `Text` or + // `Textarea`. + MinLength param.Opt[float64] `json:"minLength,omitzero"` + // The default value for this custom metadata field. This property is only required + // if `isValueRequired` property is set to `true`. The value should match the + // `type` of custom metadata field. + DefaultValue CustomMetadataFieldUpdateParamsSchemaDefaultValueUnion `json:"defaultValue,omitzero"` + // Maximum value of the field. Only set this property if field type is `Date` or + // `Number`. For `Date` type field, set the minimum date in ISO8601 string format. + // For `Number` type field, set the minimum numeric value. + MaxValue CustomMetadataFieldUpdateParamsSchemaMaxValueUnion `json:"maxValue,omitzero"` + // Minimum value of the field. Only set this property if field type is `Date` or + // `Number`. For `Date` type field, set the minimum date in ISO8601 string format. + // For `Number` type field, set the minimum numeric value. + MinValue CustomMetadataFieldUpdateParamsSchemaMinValueUnion `json:"minValue,omitzero"` + // An array of allowed values. This property is only required if `type` property is + // set to `SingleSelect` or `MultiSelect`. + SelectOptions []CustomMetadataFieldUpdateParamsSchemaSelectOptionUnion `json:"selectOptions,omitzero"` + paramObj +} + +func (r CustomMetadataFieldUpdateParamsSchema) MarshalJSON() (data []byte, err error) { + type shadow CustomMetadataFieldUpdateParamsSchema + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *CustomMetadataFieldUpdateParamsSchema) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Only one field can be non-zero. +// +// Use [param.IsOmitted] to confirm if a field is set. +type CustomMetadataFieldUpdateParamsSchemaDefaultValueUnion struct { + OfString param.Opt[string] `json:",omitzero,inline"` + OfFloat param.Opt[float64] `json:",omitzero,inline"` + OfBool param.Opt[bool] `json:",omitzero,inline"` + OfMixed []CustomMetadataFieldUpdateParamsSchemaDefaultValueMixedItemUnion `json:",omitzero,inline"` + paramUnion +} + +func (u CustomMetadataFieldUpdateParamsSchemaDefaultValueUnion) MarshalJSON() ([]byte, error) { + return param.MarshalUnion(u, u.OfString, u.OfFloat, u.OfBool, u.OfMixed) +} +func (u *CustomMetadataFieldUpdateParamsSchemaDefaultValueUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, u) +} + +func (u *CustomMetadataFieldUpdateParamsSchemaDefaultValueUnion) asAny() any { + if !param.IsOmitted(u.OfString) { + return &u.OfString.Value + } else if !param.IsOmitted(u.OfFloat) { + return &u.OfFloat.Value + } else if !param.IsOmitted(u.OfBool) { + return &u.OfBool.Value + } else if !param.IsOmitted(u.OfMixed) { + return &u.OfMixed + } + return nil +} + +// Only one field can be non-zero. +// +// Use [param.IsOmitted] to confirm if a field is set. +type CustomMetadataFieldUpdateParamsSchemaDefaultValueMixedItemUnion struct { + OfString param.Opt[string] `json:",omitzero,inline"` + OfFloat param.Opt[float64] `json:",omitzero,inline"` + OfBool param.Opt[bool] `json:",omitzero,inline"` + paramUnion +} + +func (u CustomMetadataFieldUpdateParamsSchemaDefaultValueMixedItemUnion) MarshalJSON() ([]byte, error) { + return param.MarshalUnion(u, u.OfString, u.OfFloat, u.OfBool) +} +func (u *CustomMetadataFieldUpdateParamsSchemaDefaultValueMixedItemUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, u) +} + +func (u *CustomMetadataFieldUpdateParamsSchemaDefaultValueMixedItemUnion) asAny() any { + if !param.IsOmitted(u.OfString) { + return &u.OfString.Value + } else if !param.IsOmitted(u.OfFloat) { + return &u.OfFloat.Value + } else if !param.IsOmitted(u.OfBool) { + return &u.OfBool.Value + } + return nil +} + +// Only one field can be non-zero. +// +// Use [param.IsOmitted] to confirm if a field is set. +type CustomMetadataFieldUpdateParamsSchemaMaxValueUnion struct { + OfString param.Opt[string] `json:",omitzero,inline"` + OfFloat param.Opt[float64] `json:",omitzero,inline"` + paramUnion +} + +func (u CustomMetadataFieldUpdateParamsSchemaMaxValueUnion) MarshalJSON() ([]byte, error) { + return param.MarshalUnion(u, u.OfString, u.OfFloat) +} +func (u *CustomMetadataFieldUpdateParamsSchemaMaxValueUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, u) +} + +func (u *CustomMetadataFieldUpdateParamsSchemaMaxValueUnion) asAny() any { + if !param.IsOmitted(u.OfString) { + return &u.OfString.Value + } else if !param.IsOmitted(u.OfFloat) { + return &u.OfFloat.Value + } + return nil +} + +// Only one field can be non-zero. +// +// Use [param.IsOmitted] to confirm if a field is set. +type CustomMetadataFieldUpdateParamsSchemaMinValueUnion struct { + OfString param.Opt[string] `json:",omitzero,inline"` + OfFloat param.Opt[float64] `json:",omitzero,inline"` + paramUnion +} + +func (u CustomMetadataFieldUpdateParamsSchemaMinValueUnion) MarshalJSON() ([]byte, error) { + return param.MarshalUnion(u, u.OfString, u.OfFloat) +} +func (u *CustomMetadataFieldUpdateParamsSchemaMinValueUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, u) +} + +func (u *CustomMetadataFieldUpdateParamsSchemaMinValueUnion) asAny() any { + if !param.IsOmitted(u.OfString) { + return &u.OfString.Value + } else if !param.IsOmitted(u.OfFloat) { + return &u.OfFloat.Value + } + return nil +} + +// Only one field can be non-zero. +// +// Use [param.IsOmitted] to confirm if a field is set. +type CustomMetadataFieldUpdateParamsSchemaSelectOptionUnion struct { + OfString param.Opt[string] `json:",omitzero,inline"` + OfFloat param.Opt[float64] `json:",omitzero,inline"` + OfBool param.Opt[bool] `json:",omitzero,inline"` + paramUnion +} + +func (u CustomMetadataFieldUpdateParamsSchemaSelectOptionUnion) MarshalJSON() ([]byte, error) { + return param.MarshalUnion(u, u.OfString, u.OfFloat, u.OfBool) +} +func (u *CustomMetadataFieldUpdateParamsSchemaSelectOptionUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, u) +} + +func (u *CustomMetadataFieldUpdateParamsSchemaSelectOptionUnion) asAny() any { + if !param.IsOmitted(u.OfString) { + return &u.OfString.Value + } else if !param.IsOmitted(u.OfFloat) { + return &u.OfFloat.Value + } else if !param.IsOmitted(u.OfBool) { + return &u.OfBool.Value + } + return nil +} + +type CustomMetadataFieldListParams struct { + // Set it to `true` to include deleted field objects in the API response. + IncludeDeleted param.Opt[bool] `query:"includeDeleted,omitzero" json:"-"` + paramObj +} + +// URLQuery serializes [CustomMetadataFieldListParams]'s query parameters as +// `url.Values`. +func (r CustomMetadataFieldListParams) URLQuery() (v url.Values, err error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} diff --git a/custommetadatafield_test.go b/custommetadatafield_test.go new file mode 100644 index 0000000..54469e1 --- /dev/null +++ b/custommetadatafield_test.go @@ -0,0 +1,176 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/imagekit-developer/imagekit-go" + "github.com/imagekit-developer/imagekit-go/internal/testutil" + "github.com/imagekit-developer/imagekit-go/option" +) + +func TestCustomMetadataFieldNewWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.CustomMetadataFields.New(context.TODO(), imagekit.CustomMetadataFieldNewParams{ + Label: "price", + Name: "price", + Schema: imagekit.CustomMetadataFieldNewParamsSchema{ + Type: "Number", + DefaultValue: imagekit.CustomMetadataFieldNewParamsSchemaDefaultValueUnion{ + OfString: imagekit.String("string"), + }, + IsValueRequired: imagekit.Bool(true), + MaxLength: imagekit.Float(0), + MaxValue: imagekit.CustomMetadataFieldNewParamsSchemaMaxValueUnion{ + OfFloat: imagekit.Float(3000), + }, + MinLength: imagekit.Float(0), + MinValue: imagekit.CustomMetadataFieldNewParamsSchemaMinValueUnion{ + OfFloat: imagekit.Float(1000), + }, + SelectOptions: []imagekit.CustomMetadataFieldNewParamsSchemaSelectOptionUnion{{ + OfString: imagekit.String("small"), + }, { + OfString: imagekit.String("medium"), + }, { + OfString: imagekit.String("large"), + }, { + OfFloat: imagekit.Float(30), + }, { + OfFloat: imagekit.Float(40), + }, { + OfBool: imagekit.Bool(true), + }}, + }, + }) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestCustomMetadataFieldUpdateWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.CustomMetadataFields.Update( + context.TODO(), + "id", + imagekit.CustomMetadataFieldUpdateParams{ + Label: imagekit.String("price"), + Schema: imagekit.CustomMetadataFieldUpdateParamsSchema{ + DefaultValue: imagekit.CustomMetadataFieldUpdateParamsSchemaDefaultValueUnion{ + OfString: imagekit.String("string"), + }, + IsValueRequired: imagekit.Bool(true), + MaxLength: imagekit.Float(0), + MaxValue: imagekit.CustomMetadataFieldUpdateParamsSchemaMaxValueUnion{ + OfFloat: imagekit.Float(3000), + }, + MinLength: imagekit.Float(0), + MinValue: imagekit.CustomMetadataFieldUpdateParamsSchemaMinValueUnion{ + OfFloat: imagekit.Float(1000), + }, + SelectOptions: []imagekit.CustomMetadataFieldUpdateParamsSchemaSelectOptionUnion{{ + OfString: imagekit.String("small"), + }, { + OfString: imagekit.String("medium"), + }, { + OfString: imagekit.String("large"), + }, { + OfFloat: imagekit.Float(30), + }, { + OfFloat: imagekit.Float(40), + }, { + OfBool: imagekit.Bool(true), + }}, + }, + }, + ) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestCustomMetadataFieldListWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.CustomMetadataFields.List(context.TODO(), imagekit.CustomMetadataFieldListParams{ + IncludeDeleted: imagekit.Bool(true), + }) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestCustomMetadataFieldDelete(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.CustomMetadataFields.Delete(context.TODO(), "id") + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/examples/.keep b/examples/.keep new file mode 100644 index 0000000..d8c73e9 --- /dev/null +++ b/examples/.keep @@ -0,0 +1,4 @@ +File generated from our OpenAPI spec by Stainless. + +This directory can be used to store example files demonstrating usage of this SDK. +It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index 2c8232f..0000000 --- a/examples/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# Imagekit-go SDK examples - -This module contains code examples to demonstrate different features of the SDK. Each directory(except assets) is an application which groups related features. -assets is a helper package used by different example applications. - -## Step 1 -In order to run any example, just set the following imagekit.io keys and endpoint in environment. - -- IMAGEKIT_PRIVATE_KEY -- IMAGEKIT_PUBLIC_KEY -- IMAGEKIT_ENDPOINT_URL - -``` -# Set in .bashrc or .zshrc startup file depending your shell. - -export IMAGEKIT_PRIVATE_KEY= -export IMAGEKIT_PUBLIC_KEY= -export IMAGEKIT_ENDPOINT_URL= -``` - -## Step 2 -Run any example as illustrated below. -``` -cd examples/upload -go run main.go -``` diff --git a/examples/assets/assets.go b/examples/assets/assets.go deleted file mode 100644 index cf43ef7..0000000 --- a/examples/assets/assets.go +++ /dev/null @@ -1,34 +0,0 @@ -package assets - -import ( - "context" - "embed" - "log" - "path/filepath" - - "github.com/imagekit-developer/imagekit-go" - "github.com/imagekit-developer/imagekit-go/api/uploader" -) - -//go:embed data -var Fs embed.FS -var ctx = context.Background() - -func UploadFile(ik *imagekit.ImageKit, path string) uploader.UploadResult { - var err error - - file, err := Fs.Open(path) - defer file.Close() - - if err != nil { - log.Fatal(err) - } - - _, fileName := filepath.Split(path) - - resp, err := ik.Uploader.Upload(ctx, file, uploader.UploadParam{ - FileName: fileName, - }) - - return resp.Data -} diff --git a/examples/assets/data/image.jpg b/examples/assets/data/image.jpg deleted file mode 100644 index 0e6577f..0000000 Binary files a/examples/assets/data/image.jpg and /dev/null differ diff --git a/examples/assets/data/image1.jpg b/examples/assets/data/image1.jpg deleted file mode 100644 index 9aec732..0000000 Binary files a/examples/assets/data/image1.jpg and /dev/null differ diff --git a/examples/assets/data/image2.jpg b/examples/assets/data/image2.jpg deleted file mode 100644 index 39ea28b..0000000 Binary files a/examples/assets/data/image2.jpg and /dev/null differ diff --git a/examples/assets/data/nature.jpg b/examples/assets/data/nature.jpg deleted file mode 100644 index 8552c39..0000000 Binary files a/examples/assets/data/nature.jpg and /dev/null differ diff --git a/examples/custom-metata-fields/main.go b/examples/custom-metata-fields/main.go deleted file mode 100644 index b54c3f6..0000000 --- a/examples/custom-metata-fields/main.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "context" - "log" - - "github.com/imagekit-developer/imagekit-go" - "github.com/imagekit-developer/imagekit-go/api/metadata" -) - -var ctx = context.Background() - -func main() { - var err error - ik, err := imagekit.New() - - if err != nil { - log.Fatal(err) - } - - api := ik.Metadata - - // Create custom field - fieldResp, err := api.CreateCustomField(ctx, metadata.CreateFieldParam{ - Name: "color", - Label: "Color", - Schema: metadata.Schema{ - Type: "SingleSelect", - SelectOptions: []string{"blue", "red", "green"}, - }, - }) - - log.Println(fieldResp, err) - log.Println(fieldResp.Data.Id) - - // Get custom fields - resp, err := api.CustomFields(ctx, true) - log.Println(resp, err) - - // Update custom field - // Possible to update label or schema only - updateResp, err := api.UpdateCustomField(ctx, fieldResp.Data.Id, metadata.UpdateCustomFieldParam{ - Label: "Shade", - }) - - log.Println(updateResp, err) - - // Delete custom field - delResp, err := api.DeleteCustomField(ctx, fieldResp.Data.Id) - log.Println(delResp, err) -} diff --git a/examples/go.mod b/examples/go.mod deleted file mode 100644 index b3b116d..0000000 --- a/examples/go.mod +++ /dev/null @@ -1,13 +0,0 @@ -module github.com/imagekit-developer/imagekit-go/examples - -go 1.18 - -require github.com/imagekit-developer/imagekit-go v0.0.1 - -require ( - github.com/creasty/defaults v1.6.0 // indirect - github.com/google/uuid v1.3.0 // indirect - gopkg.in/validator.v2 v2.0.1 // indirect -) - -replace github.com/imagekit-developer/imagekit-go => ../ diff --git a/examples/go.sum b/examples/go.sum deleted file mode 100644 index 13c7fb2..0000000 --- a/examples/go.sum +++ /dev/null @@ -1,15 +0,0 @@ -github.com/creasty/defaults v1.6.0 h1:ltuE9cfphUtlrBeomuu8PEyISTXnxqkBIoQfXgv7BSc= -github.com/creasty/defaults v1.6.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/validator.v2 v2.0.1 h1:xF0KWyGWXm/LM2G1TrEjqOu4pa6coO9AlWSf3msVfDY= -gopkg.in/validator.v2 v2.0.1/go.mod h1:lIUZBlB3Im4s/eYp39Ry/wkR02yOPhZ9IwIRBjuPuG8= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= diff --git a/examples/media/bulk-job-status/main.go b/examples/media/bulk-job-status/main.go deleted file mode 100644 index fdc00b9..0000000 --- a/examples/media/bulk-job-status/main.go +++ /dev/null @@ -1,37 +0,0 @@ -package main - -import ( - "context" - "log" - - "github.com/imagekit-developer/imagekit-go" - "github.com/imagekit-developer/imagekit-go/api/media" -) - -var ctx = context.Background() - -func main() { - var err error - ik, err := imagekit.New() - - if err != nil { - log.Fatal(err) - } - - // Create folder - resp, err := ik.Media.CreateFolder(ctx, media.CreateFolderParam{ - FolderName: "new", - ParentFolderPath: "/", - }) - log.Println(resp, err) - - // Move Folder - moveResp, err := ik.Media.MoveFolder(ctx, media.MoveFolderParam{ - SourceFolderPath: "/new", - DestinationPath: "/nature", - }) - - // Get job status - jobStatus, err := ik.Media.BulkJobStatus(ctx, moveResp.Data.JobId) - log.Println(jobStatus.Data, err) -} diff --git a/examples/media/copy-move-rename/main.go b/examples/media/copy-move-rename/main.go deleted file mode 100644 index 09d68a8..0000000 --- a/examples/media/copy-move-rename/main.go +++ /dev/null @@ -1,48 +0,0 @@ -package main - -import ( - "context" - "log" - - "github.com/imagekit-developer/imagekit-go" - "github.com/imagekit-developer/imagekit-go/api/media" - "github.com/imagekit-developer/imagekit-go/examples/assets" -) - -var ctx = context.Background() - -func main() { - var err error - ik, err := imagekit.New() - - if err != nil { - log.Fatal(err) - } - - var api = ik.Media - - file := assets.UploadFile(ik, "data/nature.jpg") - file1 := assets.UploadFile(ik, "data/image1.jpg") - - // Copy file - resp, err := api.CopyFile(ctx, media.CopyFileParam{ - SourcePath: file.FilePath, - DestinationPath: "/target/", - }) - log.Println(resp, err) - - // Move file - resp, err = api.MoveFile(ctx, media.MoveFileParam{ - SourcePath: file.FilePath, - DestinationPath: "/newpath/", - }) - log.Println(resp, err) - - // Rename file - renameResp, err := api.RenameFile(ctx, media.RenameFileParam{ - FilePath: file1.FilePath, - NewFileName: "sample.jpg", - PurgeCache: true, // optionally purge cache - }) - log.Println(renameResp, err) -} diff --git a/examples/media/file-versions/main.go b/examples/media/file-versions/main.go deleted file mode 100644 index 711bd88..0000000 --- a/examples/media/file-versions/main.go +++ /dev/null @@ -1,50 +0,0 @@ -package main - -import ( - "context" - "log" - - "github.com/imagekit-developer/imagekit-go" - "github.com/imagekit-developer/imagekit-go/api/media" -) - -var ctx = context.Background() - -func main() { - var err error - ik, err := imagekit.New() - - if err != nil { - log.Fatal(err) - } - - var api = ik.Media - // replace fileId and versionId with real values - fileId := "" - versionId := "" - - // Get all versions - versionsResp, err := api.FileVersions(ctx, media.FileVersionsParam{ - FileId: fileId, - }) - - log.Println(versionsResp, err) - - // Get file version - versionsResp, err = api.FileVersions(ctx, media.FileVersionsParam{ - FileId: fileId, - VersionId: versionId, - }) - log.Println(versionsResp, err) - - // Delete file version - resp, err := api.DeleteFileVersion(ctx, fileId, versionId) - log.Println(resp, err) - - // Delete bulk files - delBulkResp, err := api.DeleteBulkFiles(ctx, media.FileIdsParam{ - FileIds: []string{"x", "y"}, // replace with actual values - }) - log.Println(delBulkResp, err) - -} diff --git a/examples/media/folders/main.go b/examples/media/folders/main.go deleted file mode 100644 index 0634697..0000000 --- a/examples/media/folders/main.go +++ /dev/null @@ -1,48 +0,0 @@ -package main - -import ( - "context" - "log" - - "github.com/imagekit-developer/imagekit-go" - "github.com/imagekit-developer/imagekit-go/api/media" -) - -var ctx = context.Background() - -func main() { - var err error - ik, err := imagekit.New() - - if err != nil { - log.Fatal(err) - } - - // Create folder - resp, err := ik.Media.CreateFolder(ctx, media.CreateFolderParam{ - FolderName: "new", - ParentFolderPath: "/", - }) - log.Println(resp, err) - - // Copy folder - folderResp, err := ik.Media.CopyFolder(ctx, media.CopyFolderParam{ - SourceFolderPath: "/new", - DestinationPath: "/sample", - IncludeFileVersions: true, - }) - log.Println(folderResp, err) - - // Move Folder - folderResp, err = ik.Media.MoveFolder(ctx, media.MoveFolderParam{ - SourceFolderPath: "/new", - DestinationPath: "/nature", - }) - log.Println(folderResp, err) - - // Delete Folder - resp, err = ik.Media.DeleteFolder(ctx, media.DeleteFolderParam{ - FolderPath: "/nature", - }) - log.Println(resp, err) -} diff --git a/examples/media/get-file/main.go b/examples/media/get-file/main.go deleted file mode 100644 index 6b957da..0000000 --- a/examples/media/get-file/main.go +++ /dev/null @@ -1,29 +0,0 @@ -package main - -import ( - "context" - "log" - - "github.com/imagekit-developer/imagekit-go" -) - -var ctx = context.Background() - -func main() { - var err error - ik, err := imagekit.New() - - if err != nil { - log.Fatal(err) - } - - var api = ik.Media - - // replace fileId with real value - fileId := "" - - // Get file detail by id - fileResp, err := api.FileById(ctx, fileId) - log.Println(fileResp, err) - -} diff --git a/examples/media/main.go b/examples/media/main.go deleted file mode 100644 index 1920abd..0000000 --- a/examples/media/main.go +++ /dev/null @@ -1,32 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - - "github.com/imagekit-developer/imagekit-go" - "github.com/imagekit-developer/imagekit-go/url" -) - -var ctx = context.Background() - -func main() { - var err error - ik, err := imagekit.New() - - if err != nil { - log.Fatal(err) - } - - // uri := "https://ik.imagekit.io/dk1m7xkgi/test1_FDy44QvOp.gif" - path := "test1_FDy44QvOp.gif" - - s, err := ik.Url(url.UrlParam{ - Path: path, - Signed: true, - ExpireSeconds: 3600 * 24, - }) - - fmt.Println(s, err) -} diff --git a/examples/media/purge-cache/main.go b/examples/media/purge-cache/main.go deleted file mode 100644 index 0a5e20c..0000000 --- a/examples/media/purge-cache/main.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -import ( - "context" - "log" - - "github.com/imagekit-developer/imagekit-go" - "github.com/imagekit-developer/imagekit-go/api/media" - "github.com/imagekit-developer/imagekit-go/examples/assets" -) - -var ctx = context.Background() - -func main() { - var err error - ik, err := imagekit.New() - - if err != nil { - log.Fatal(err) - } - - file := assets.UploadFile(ik, "data/nature.jpg") - - log.Println(file.Url) - - var param = media.PurgeCacheParam{ - Url: file.Url, - } - - response, err := ik.Media.PurgeCache(ctx, param) - log.Println(response, err) - - statusResp, err := ik.Media.PurgeCacheStatus(ctx, response.Data.RequestId) - - log.Println(statusResp.Data.Status, err) -} diff --git a/examples/media/restore-version/main.go b/examples/media/restore-version/main.go deleted file mode 100644 index 6bef219..0000000 --- a/examples/media/restore-version/main.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - "context" - "log" - - "github.com/imagekit-developer/imagekit-go" - "github.com/imagekit-developer/imagekit-go/api/media" -) - -var ctx = context.Background() - -func main() { - var err error - ik, err := imagekit.New() - - if err != nil { - log.Fatal(err) - } - - // replace FileId and VersionId with real values - resp, err := ik.Media.RestoreVersion(ctx, media.FileVersionsParam{ - FileId: "", - VersionId: "", - }) - log.Println(resp, err) -} diff --git a/examples/media/search-update-delete/main.go b/examples/media/search-update-delete/main.go deleted file mode 100644 index d33d926..0000000 --- a/examples/media/search-update-delete/main.go +++ /dev/null @@ -1,52 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - - "github.com/imagekit-developer/imagekit-go" - "github.com/imagekit-developer/imagekit-go/api/media" - "github.com/imagekit-developer/imagekit-go/api/uploader" - "github.com/imagekit-developer/imagekit-go/examples/assets" -) - -var ctx = context.Background() - -func main() { - var err error - ik, err := imagekit.New() - - if err != nil { - log.Fatal(err) - } - - var api = ik.Media - var files []uploader.UploadResult - - for _, f := range []string{"data/nature.jpg", "data/nature.jpg"} { - files = append(files, assets.UploadFile(ik, f)) - } - - // Get all files - filesResp, err := api.Files(ctx, media.FilesParam{Path: "/"}) - log.Println(string(filesResp.Body())) - - if err != nil { - log.Fatal(err) - } - - for _, file := range filesResp.Data { - fmt.Println("files resp ", file.FileId, file.VersionInfo) - } - - // Delete a file - delResp, err := api.DeleteFile(ctx, files[0].FileId) - log.Println(delResp, err) - - // Delete multiple files - delBulkResp, err := api.DeleteBulkFiles(ctx, media.FileIdsParam{ - FileIds: []string{files[1].FileId}, - }) - log.Println(delBulkResp, err) -} diff --git a/examples/media/tagging/main.go b/examples/media/tagging/main.go deleted file mode 100644 index eaf5002..0000000 --- a/examples/media/tagging/main.go +++ /dev/null @@ -1,45 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - - "github.com/imagekit-developer/imagekit-go" - "github.com/imagekit-developer/imagekit-go/api/media" - "github.com/imagekit-developer/imagekit-go/examples/assets" -) - -var ctx = context.Background() - -func main() { - var err error - ik, err := imagekit.New() - - if err != nil { - log.Fatal(err) - } - - file := assets.UploadFile(ik, "data/nature.jpg") - - var api = ik.Media - // Add tags - tagsResp, err := api.UpdateFile(ctx, file.FileId, media.UpdateFileParam{ - Tags: []string{"natural", "mountains", "scene", "day"}, - }) - fmt.Println(tagsResp, err) - - // Remove tags - remTagResp, err := api.RemoveTags(ctx, media.TagsParam{ - FileIds: []string{file.FileId}, - Tags: []string{"scene", "day"}, - }) - log.Println(remTagResp, err) - - // Remove AI tags - remTagResp, err = api.RemoveAITags(ctx, media.AITagsParam{ - FileIds: []string{file.FileId}, - AITags: []string{"x", "y"}, // replace with real AI tags - }) - log.Println(remTagResp, err) -} diff --git a/examples/metadata/main.go b/examples/metadata/main.go deleted file mode 100644 index 251f5d7..0000000 --- a/examples/metadata/main.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -import ( - "context" - "log" - - "github.com/imagekit-developer/imagekit-go" - "github.com/imagekit-developer/imagekit-go/examples/assets" -) - -var ctx = context.Background() - -func main() { - var err error - ik, err := imagekit.New() - - if err != nil { - log.Fatal(err) - } - - api := ik.Metadata - - file := assets.UploadFile(ik, "data/nature.jpg") - - // metadata from url - resp, err := api.FromUrl(ctx, file.Url) - - log.Println(resp, err) - log.Println(resp.Data.Height, resp.Data.Width) - - // metadata from fileId - resp, err = api.FromFile(ctx, file.FileId) - log.Println(resp, err) - log.Println(resp.Data.Height, resp.Data.Width) - -} diff --git a/examples/upload/main.go b/examples/upload/main.go deleted file mode 100644 index 52b7b49..0000000 --- a/examples/upload/main.go +++ /dev/null @@ -1,66 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - - "github.com/imagekit-developer/imagekit-go" - "github.com/imagekit-developer/imagekit-go/api/uploader" - "github.com/imagekit-developer/imagekit-go/examples/assets" -) - -var ctx = context.Background() - -const Base64Image = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" -const url = "https://images.pexels.com/photos/247676/pexels-photo-247676.jpeg?auto=compress&cs=tinysrgb&dpr=3&h=750&w=1260" - -// Upload base64 encoded file -func uploadBase64(ik *imagekit.ImageKit) { - resp, err := ik.Uploader.Upload(ctx, Base64Image, uploader.UploadParam{ - FileName: "test.gif", - }) - - fmt.Println(resp, err) -} - -// Upload using a reader -func uploadReader(ik *imagekit.ImageKit) { - var err error - - file, err := assets.Fs.Open("data/nature.jpg") - defer file.Close() - - if err != nil { - log.Fatal(err) - } - - resp, err := ik.Uploader.Upload(ctx, file, uploader.UploadParam{ - FileName: "test1.gif", - }) - - fmt.Println(resp, err) -} - -// Upload using a url -func uploadUrl(ik *imagekit.ImageKit) { - var err error - - resp, err := ik.Uploader.Upload(ctx, url, uploader.UploadParam{ - FileName: "test2.gif", - }) - - fmt.Println(resp, err) -} - -func main() { - ik, err := imagekit.New() - - if err != nil { - log.Fatal(err) - } - - uploadBase64(ik) - uploadReader(ik) - uploadUrl(ik) -} diff --git a/examples/url-generation/main.go b/examples/url-generation/main.go deleted file mode 100644 index 5760bc1..0000000 --- a/examples/url-generation/main.go +++ /dev/null @@ -1,33 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "github.com/imagekit-developer/imagekit-go" - ikurl "github.com/imagekit-developer/imagekit-go/url" -) - -func main() { - ik, err := imagekit.New() - - if err != nil { - log.Fatal(err) - } - - url, err := ik.Url(ikurl.UrlParam{ - Path: "/default-image.jpg", - Transformations: []map[string]any{ - { - "height": 300, - "width": 300, - "overlayImage": "test2_5r9tRj4L4.gif", - "overlayWidth": 100, - "x": 0, - "overlayImageBorder": "10_CDDC39", - }, - }, - }) - - fmt.Println(url, err) -} diff --git a/field.go b/field.go new file mode 100644 index 0000000..5e829ae --- /dev/null +++ b/field.go @@ -0,0 +1,45 @@ +package imagekit + +import ( + "github.com/imagekit-developer/imagekit-go/packages/param" + "io" + "time" +) + +func String(s string) param.Opt[string] { return param.NewOpt(s) } +func Int(i int64) param.Opt[int64] { return param.NewOpt(i) } +func Bool(b bool) param.Opt[bool] { return param.NewOpt(b) } +func Float(f float64) param.Opt[float64] { return param.NewOpt(f) } +func Time(t time.Time) param.Opt[time.Time] { return param.NewOpt(t) } + +func Opt[T comparable](v T) param.Opt[T] { return param.NewOpt(v) } +func Ptr[T any](v T) *T { return &v } + +func IntPtr(v int64) *int64 { return &v } +func BoolPtr(v bool) *bool { return &v } +func FloatPtr(v float64) *float64 { return &v } +func StringPtr(v string) *string { return &v } +func TimePtr(v time.Time) *time.Time { return &v } + +func NewFile(rdr io.Reader, filename string, contentType string) file { + return file{rdr, filename, contentType} +} + +type file struct { + io.Reader + name string + contentType string +} + +func (f file) Filename() string { + if f.name != "" { + return f.name + } else if named, ok := f.Reader.(interface{ Name() string }); ok { + return named.Name() + } + return "" +} + +func (f file) ContentType() string { + return f.contentType +} diff --git a/file.go b/file.go new file mode 100644 index 0000000..c976cd4 --- /dev/null +++ b/file.go @@ -0,0 +1,1456 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "mime/multipart" + "net/http" + "time" + + "github.com/imagekit-developer/imagekit-go/internal/apiform" + "github.com/imagekit-developer/imagekit-go/internal/apijson" + shimjson "github.com/imagekit-developer/imagekit-go/internal/encoding/json" + "github.com/imagekit-developer/imagekit-go/internal/requestconfig" + "github.com/imagekit-developer/imagekit-go/option" + "github.com/imagekit-developer/imagekit-go/packages/param" + "github.com/imagekit-developer/imagekit-go/packages/respjson" + "github.com/imagekit-developer/imagekit-go/shared" + "github.com/imagekit-developer/imagekit-go/shared/constant" +) + +// FileService contains methods and other services that help with interacting with +// the ImageKit API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewFileService] method instead. +type FileService struct { + Options []option.RequestOption + Bulk FileBulkService + Versions FileVersionService + Metadata FileMetadataService +} + +// NewFileService generates a new service that applies the given options to each +// request. These options are applied after the parent client's options (if there +// is one), and before any request-specific options. +func NewFileService(opts ...option.RequestOption) (r FileService) { + r = FileService{} + r.Options = opts + r.Bulk = NewFileBulkService(opts...) + r.Versions = NewFileVersionService(opts...) + r.Metadata = NewFileMetadataService(opts...) + return +} + +// This API updates the details or attributes of the current version of the file. +// You can update `tags`, `customCoordinates`, `customMetadata`, publication +// status, remove existing `AITags` and apply extensions using this API. +func (r *FileService) Update(ctx context.Context, fileID string, body FileUpdateParams, opts ...option.RequestOption) (res *FileUpdateResponse, err error) { + opts = append(r.Options[:], opts...) + if fileID == "" { + err = errors.New("missing required fileId parameter") + return + } + path := fmt.Sprintf("v1/files/%s/details", fileID) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPatch, path, body, &res, opts...) + return +} + +// This API deletes the file and all its file versions permanently. +// +// Note: If a file or specific transformation has been requested in the past, then +// the response is cached. Deleting a file does not purge the cache. You can purge +// the cache using purge cache API. +func (r *FileService) Delete(ctx context.Context, fileID string, opts ...option.RequestOption) (err error) { + opts = append(r.Options[:], opts...) + opts = append([]option.RequestOption{option.WithHeader("Accept", "")}, opts...) + if fileID == "" { + err = errors.New("missing required fileId parameter") + return + } + path := fmt.Sprintf("v1/files/%s", fileID) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, nil, opts...) + return +} + +// This will copy a file from one folder to another. +// +// Note: If any file at the destination has the same name as the source file, then +// the source file and its versions (if `includeFileVersions` is set to true) will +// be appended to the destination file version history. +func (r *FileService) Copy(ctx context.Context, body FileCopyParams, opts ...option.RequestOption) (res *FileCopyResponse, err error) { + opts = append(r.Options[:], opts...) + path := "v1/files/copy" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +// This API returns an object with details or attributes about the current version +// of the file. +func (r *FileService) Get(ctx context.Context, fileID string, opts ...option.RequestOption) (res *File, err error) { + opts = append(r.Options[:], opts...) + if fileID == "" { + err = errors.New("missing required fileId parameter") + return + } + path := fmt.Sprintf("v1/files/%s/details", fileID) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return +} + +// This will move a file and all its versions from one folder to another. +// +// Note: If any file at the destination has the same name as the source file, then +// the source file and its versions will be appended to the destination file. +func (r *FileService) Move(ctx context.Context, body FileMoveParams, opts ...option.RequestOption) (res *FileMoveResponse, err error) { + opts = append(r.Options[:], opts...) + path := "v1/files/move" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +// You can rename an already existing file in the media library using rename file +// API. This operation would rename all file versions of the file. +// +// Note: The old URLs will stop working. The file/file version URLs cached on CDN +// will continue to work unless a purge is requested. +func (r *FileService) Rename(ctx context.Context, body FileRenameParams, opts ...option.RequestOption) (res *FileRenameResponse, err error) { + opts = append(r.Options[:], opts...) + path := "v1/files/rename" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPut, path, body, &res, opts...) + return +} + +// ImageKit.io allows you to upload files directly from both the server and client +// sides. For server-side uploads, private API key authentication is used. For +// client-side uploads, generate a one-time `token`, `signature`, and `expire` from +// your secure backend using private API. +// [Learn more](/docs/api-reference/upload-file/upload-file#how-to-implement-client-side-file-upload) +// about how to implement client-side file upload. +// +// The [V2 API](/docs/api-reference/upload-file/upload-file-v2) enhances security +// by verifying the entire payload using JWT. +// +// **File size limit** \ +// On the free plan, the maximum upload file sizes are 20MB for images, audio, and raw +// files and 100MB for videos. On the paid plan, these limits increase to 40MB for images, +// audio, and raw files and 2GB for videos. These limits can be further increased with +// higher-tier plans. +// +// **Version limit** \ +// A file can have a maximum of 100 versions. +// +// **Demo applications** +// +// - A full-fledged +// [upload widget using Uppy](https://github.com/imagekit-samples/uppy-uploader), +// supporting file selections from local storage, URL, Dropbox, Google Drive, +// Instagram, and more. +// - [Quick start guides](/docs/quick-start-guides) for various frameworks and +// technologies. +func (r *FileService) Upload(ctx context.Context, body FileUploadParams, opts ...option.RequestOption) (res *FileUploadResponse, err error) { + opts = append(r.Options[:], opts...) + opts = append([]option.RequestOption{option.WithBaseURL("https://upload.imagekit.io/")}, opts...) + path := "api/v1/files/upload" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +// Object containing details of a file or file version. +type File struct { + // An array of tags assigned to the file by auto tagging. + AITags []FileAITag `json:"AITags,nullable"` + // Date and time when the file was uploaded. The date and time is in ISO8601 + // format. + CreatedAt time.Time `json:"createdAt" format:"date-time"` + // An string with custom coordinates of the file. + CustomCoordinates string `json:"customCoordinates,nullable"` + // An object with custom metadata for the file. + CustomMetadata map[string]any `json:"customMetadata"` + // Optional text to describe the contents of the file. Can be set by the user or + // the ai-auto-description extension. + Description string `json:"description"` + // Unique identifier of the asset. + FileID string `json:"fileId"` + // Path of the file. This is the path you would use in the URL to access the file. + // For example, if the file is at the root of the media library, the path will be + // `/file.jpg`. If the file is inside a folder named `images`, the path will be + // `/images/file.jpg`. + FilePath string `json:"filePath"` + // Type of the file. Possible values are `image`, `non-image`. + FileType string `json:"fileType"` + // Specifies if the image has an alpha channel. + HasAlpha bool `json:"hasAlpha"` + // Height of the file. + Height float64 `json:"height"` + // Specifies if the file is private or not. + IsPrivateFile bool `json:"isPrivateFile"` + // Specifies if the file is published or not. + IsPublished bool `json:"isPublished"` + // MIME type of the file. + Mime string `json:"mime"` + // Name of the asset. + Name string `json:"name"` + // Size of the file in bytes. + Size float64 `json:"size"` + // An array of tags assigned to the file. Tags are used to search files in the + // media library. + Tags []string `json:"tags,nullable"` + // URL of the thumbnail image. This URL is used to access the thumbnail image of + // the file in the media library. + Thumbnail string `json:"thumbnail" format:"uri"` + // Type of the asset. + // + // Any of "file", "file-version". + Type FileType `json:"type"` + // Date and time when the file was last updated. The date and time is in ISO8601 + // format. + UpdatedAt time.Time `json:"updatedAt" format:"date-time"` + // URL of the file. + URL string `json:"url" format:"uri"` + // An object with details of the file version. + VersionInfo FileVersionInfo `json:"versionInfo"` + // Width of the file. + Width float64 `json:"width"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + AITags respjson.Field + CreatedAt respjson.Field + CustomCoordinates respjson.Field + CustomMetadata respjson.Field + Description respjson.Field + FileID respjson.Field + FilePath respjson.Field + FileType respjson.Field + HasAlpha respjson.Field + Height respjson.Field + IsPrivateFile respjson.Field + IsPublished respjson.Field + Mime respjson.Field + Name respjson.Field + Size respjson.Field + Tags respjson.Field + Thumbnail respjson.Field + Type respjson.Field + UpdatedAt respjson.Field + URL respjson.Field + VersionInfo respjson.Field + Width respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r File) RawJSON() string { return r.JSON.raw } +func (r *File) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type FileAITag struct { + // Confidence score of the tag. + Confidence float64 `json:"confidence"` + // Name of the tag. + Name string `json:"name"` + // Source of the tag. Possible values are `google-auto-tagging` and + // `aws-auto-tagging`. + Source string `json:"source"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Confidence respjson.Field + Name respjson.Field + Source respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r FileAITag) RawJSON() string { return r.JSON.raw } +func (r *FileAITag) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Type of the asset. +type FileType string + +const ( + FileTypeFile FileType = "file" + FileTypeFileVersion FileType = "file-version" +) + +// An object with details of the file version. +type FileVersionInfo struct { + // Unique identifier of the file version. + ID string `json:"id"` + // Name of the file version. + Name string `json:"name"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ID respjson.Field + Name respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r FileVersionInfo) RawJSON() string { return r.JSON.raw } +func (r *FileVersionInfo) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type Folder struct { + // Date and time when the folder was created. The date and time is in ISO8601 + // format. + CreatedAt time.Time `json:"createdAt" format:"date-time"` + // Unique identifier of the asset. + FolderID string `json:"folderId"` + // Path of the folder. This is the path you would use in the URL to access the + // folder. For example, if the folder is at the root of the media library, the path + // will be /folder. If the folder is inside another folder named images, the path + // will be /images/folder. + FolderPath string `json:"folderPath"` + // Name of the asset. + Name string `json:"name"` + // Type of the asset. + // + // Any of "folder". + Type FolderType `json:"type"` + // Date and time when the folder was last updated. The date and time is in ISO8601 + // format. + UpdatedAt time.Time `json:"updatedAt" format:"date-time"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + CreatedAt respjson.Field + FolderID respjson.Field + FolderPath respjson.Field + Name respjson.Field + Type respjson.Field + UpdatedAt respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r Folder) RawJSON() string { return r.JSON.raw } +func (r *Folder) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Type of the asset. +type FolderType string + +const ( + FolderTypeFolder FolderType = "folder" +) + +// JSON object containing metadata. +type Metadata struct { + // The audio codec used in the video (only for video). + AudioCodec string `json:"audioCodec"` + // The bit rate of the video in kbps (only for video). + BitRate int64 `json:"bitRate"` + // The density of the image in DPI. + Density int64 `json:"density"` + // The duration of the video in seconds (only for video). + Duration int64 `json:"duration"` + Exif MetadataExif `json:"exif"` + // The format of the file (e.g., 'jpg', 'mp4'). + Format string `json:"format"` + // Indicates if the image has a color profile. + HasColorProfile bool `json:"hasColorProfile"` + // Indicates if the image contains transparent areas. + HasTransparency bool `json:"hasTransparency"` + // The height of the image or video in pixels. + Height int64 `json:"height"` + // Perceptual hash of the image. + PHash string `json:"pHash"` + // The quality indicator of the image. + Quality int64 `json:"quality"` + // The file size in bytes. + Size int64 `json:"size"` + // The video codec used in the video (only for video). + VideoCodec string `json:"videoCodec"` + // The width of the image or video in pixels. + Width int64 `json:"width"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + AudioCodec respjson.Field + BitRate respjson.Field + Density respjson.Field + Duration respjson.Field + Exif respjson.Field + Format respjson.Field + HasColorProfile respjson.Field + HasTransparency respjson.Field + Height respjson.Field + PHash respjson.Field + Quality respjson.Field + Size respjson.Field + VideoCodec respjson.Field + Width respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r Metadata) RawJSON() string { return r.JSON.raw } +func (r *Metadata) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type MetadataExif struct { + // Object containing Exif details. + Exif MetadataExifExif `json:"exif"` + // Object containing GPS information. + Gps MetadataExifGps `json:"gps"` + // Object containing EXIF image information. + Image MetadataExifImage `json:"image"` + // JSON object. + Interoperability MetadataExifInteroperability `json:"interoperability"` + Makernote map[string]any `json:"makernote"` + // Object containing Thumbnail information. + Thumbnail MetadataExifThumbnail `json:"thumbnail"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Exif respjson.Field + Gps respjson.Field + Image respjson.Field + Interoperability respjson.Field + Makernote respjson.Field + Thumbnail respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r MetadataExif) RawJSON() string { return r.JSON.raw } +func (r *MetadataExif) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Object containing Exif details. +type MetadataExifExif struct { + ApertureValue float64 `json:"ApertureValue"` + ColorSpace int64 `json:"ColorSpace"` + CreateDate string `json:"CreateDate"` + CustomRendered int64 `json:"CustomRendered"` + DateTimeOriginal string `json:"DateTimeOriginal"` + ExifImageHeight int64 `json:"ExifImageHeight"` + ExifImageWidth int64 `json:"ExifImageWidth"` + ExifVersion string `json:"ExifVersion"` + ExposureCompensation float64 `json:"ExposureCompensation"` + ExposureMode int64 `json:"ExposureMode"` + ExposureProgram int64 `json:"ExposureProgram"` + ExposureTime float64 `json:"ExposureTime"` + Flash int64 `json:"Flash"` + FlashpixVersion string `json:"FlashpixVersion"` + FNumber float64 `json:"FNumber"` + FocalLength int64 `json:"FocalLength"` + FocalPlaneResolutionUnit int64 `json:"FocalPlaneResolutionUnit"` + FocalPlaneXResolution float64 `json:"FocalPlaneXResolution"` + FocalPlaneYResolution float64 `json:"FocalPlaneYResolution"` + InteropOffset int64 `json:"InteropOffset"` + ISO int64 `json:"ISO"` + MeteringMode int64 `json:"MeteringMode"` + SceneCaptureType int64 `json:"SceneCaptureType"` + ShutterSpeedValue float64 `json:"ShutterSpeedValue"` + SubSecTime string `json:"SubSecTime"` + WhiteBalance int64 `json:"WhiteBalance"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ApertureValue respjson.Field + ColorSpace respjson.Field + CreateDate respjson.Field + CustomRendered respjson.Field + DateTimeOriginal respjson.Field + ExifImageHeight respjson.Field + ExifImageWidth respjson.Field + ExifVersion respjson.Field + ExposureCompensation respjson.Field + ExposureMode respjson.Field + ExposureProgram respjson.Field + ExposureTime respjson.Field + Flash respjson.Field + FlashpixVersion respjson.Field + FNumber respjson.Field + FocalLength respjson.Field + FocalPlaneResolutionUnit respjson.Field + FocalPlaneXResolution respjson.Field + FocalPlaneYResolution respjson.Field + InteropOffset respjson.Field + ISO respjson.Field + MeteringMode respjson.Field + SceneCaptureType respjson.Field + ShutterSpeedValue respjson.Field + SubSecTime respjson.Field + WhiteBalance respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r MetadataExifExif) RawJSON() string { return r.JSON.raw } +func (r *MetadataExifExif) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Object containing GPS information. +type MetadataExifGps struct { + GpsVersionID []int64 `json:"GPSVersionID"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + GpsVersionID respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r MetadataExifGps) RawJSON() string { return r.JSON.raw } +func (r *MetadataExifGps) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Object containing EXIF image information. +type MetadataExifImage struct { + ExifOffset int64 `json:"ExifOffset"` + GpsInfo int64 `json:"GPSInfo"` + Make string `json:"Make"` + Model string `json:"Model"` + ModifyDate string `json:"ModifyDate"` + Orientation int64 `json:"Orientation"` + ResolutionUnit int64 `json:"ResolutionUnit"` + Software string `json:"Software"` + XResolution int64 `json:"XResolution"` + YCbCrPositioning int64 `json:"YCbCrPositioning"` + YResolution int64 `json:"YResolution"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ExifOffset respjson.Field + GpsInfo respjson.Field + Make respjson.Field + Model respjson.Field + ModifyDate respjson.Field + Orientation respjson.Field + ResolutionUnit respjson.Field + Software respjson.Field + XResolution respjson.Field + YCbCrPositioning respjson.Field + YResolution respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r MetadataExifImage) RawJSON() string { return r.JSON.raw } +func (r *MetadataExifImage) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// JSON object. +type MetadataExifInteroperability struct { + InteropIndex string `json:"InteropIndex"` + InteropVersion string `json:"InteropVersion"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + InteropIndex respjson.Field + InteropVersion respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r MetadataExifInteroperability) RawJSON() string { return r.JSON.raw } +func (r *MetadataExifInteroperability) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Object containing Thumbnail information. +type MetadataExifThumbnail struct { + Compression int64 `json:"Compression"` + ResolutionUnit int64 `json:"ResolutionUnit"` + ThumbnailLength int64 `json:"ThumbnailLength"` + ThumbnailOffset int64 `json:"ThumbnailOffset"` + XResolution int64 `json:"XResolution"` + YResolution int64 `json:"YResolution"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Compression respjson.Field + ResolutionUnit respjson.Field + ThumbnailLength respjson.Field + ThumbnailOffset respjson.Field + XResolution respjson.Field + YResolution respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r MetadataExifThumbnail) RawJSON() string { return r.JSON.raw } +func (r *MetadataExifThumbnail) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Only one field can be non-zero. +// +// Use [param.IsOmitted] to confirm if a field is set. +type UpdateFileDetailsRequestUnionParam struct { + OfUpdateFileDetails *UpdateFileDetailsRequestUpdateFileDetailsParam `json:",omitzero,inline"` + OfChangePublicationStatus *UpdateFileDetailsRequestChangePublicationStatusParam `json:",omitzero,inline"` + paramUnion +} + +func (u UpdateFileDetailsRequestUnionParam) MarshalJSON() ([]byte, error) { + return param.MarshalUnion(u, u.OfUpdateFileDetails, u.OfChangePublicationStatus) +} +func (u *UpdateFileDetailsRequestUnionParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, u) +} + +func (u *UpdateFileDetailsRequestUnionParam) asAny() any { + if !param.IsOmitted(u.OfUpdateFileDetails) { + return u.OfUpdateFileDetails + } else if !param.IsOmitted(u.OfChangePublicationStatus) { + return u.OfChangePublicationStatus + } + return nil +} + +type UpdateFileDetailsRequestUpdateFileDetailsParam struct { + // Define an important area in the image in the format `x,y,width,height` e.g. + // `10,10,100,100`. Send `null` to unset this value. + CustomCoordinates param.Opt[string] `json:"customCoordinates,omitzero"` + // Optional text to describe the contents of the file. + Description param.Opt[string] `json:"description,omitzero"` + // The final status of extensions after they have completed execution will be + // delivered to this endpoint as a POST request. + // [Learn more](/docs/api-reference/digital-asset-management-dam/managing-assets/update-file-details#webhook-payload-structure) + // about the webhook payload structure. + WebhookURL param.Opt[string] `json:"webhookUrl,omitzero" format:"uri"` + // An array of tags associated with the file, such as `["tag1", "tag2"]`. Send + // `null` to unset all tags associated with the file. + Tags []string `json:"tags,omitzero"` + // A key-value data to be associated with the asset. To unset a key, send `null` + // value for that key. Before setting any custom metadata on an asset you have to + // create the field using custom metadata fields API. + CustomMetadata map[string]any `json:"customMetadata,omitzero"` + // Array of extensions to be applied to the asset. Each extension can be configured + // with specific parameters based on the extension type. + Extensions shared.ExtensionsParam `json:"extensions,omitzero"` + // An array of AITags associated with the file that you want to remove, e.g. + // `["car", "vehicle", "motorsports"]`. + // + // If you want to remove all AITags associated with the file, send a string - + // "all". + // + // Note: The remove operation for `AITags` executes before any of the `extensions` + // are processed. + RemoveAITags UpdateFileDetailsRequestUpdateFileDetailsRemoveAITagsUnionParam `json:"removeAITags,omitzero"` + paramObj +} + +func (r UpdateFileDetailsRequestUpdateFileDetailsParam) MarshalJSON() (data []byte, err error) { + type shadow UpdateFileDetailsRequestUpdateFileDetailsParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *UpdateFileDetailsRequestUpdateFileDetailsParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Only one field can be non-zero. +// +// Use [param.IsOmitted] to confirm if a field is set. +type UpdateFileDetailsRequestUpdateFileDetailsRemoveAITagsUnionParam struct { + OfStringArray []string `json:",omitzero,inline"` + // Construct this variant with constant.ValueOf[constant.All]() + OfAll constant.All `json:",omitzero,inline"` + paramUnion +} + +func (u UpdateFileDetailsRequestUpdateFileDetailsRemoveAITagsUnionParam) MarshalJSON() ([]byte, error) { + return param.MarshalUnion(u, u.OfStringArray, u.OfAll) +} +func (u *UpdateFileDetailsRequestUpdateFileDetailsRemoveAITagsUnionParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, u) +} + +func (u *UpdateFileDetailsRequestUpdateFileDetailsRemoveAITagsUnionParam) asAny() any { + if !param.IsOmitted(u.OfStringArray) { + return &u.OfStringArray + } else if !param.IsOmitted(u.OfAll) { + return &u.OfAll + } + return nil +} + +type UpdateFileDetailsRequestChangePublicationStatusParam struct { + // Configure the publication status of a file and its versions. + Publish UpdateFileDetailsRequestChangePublicationStatusPublishParam `json:"publish,omitzero"` + paramObj +} + +func (r UpdateFileDetailsRequestChangePublicationStatusParam) MarshalJSON() (data []byte, err error) { + type shadow UpdateFileDetailsRequestChangePublicationStatusParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *UpdateFileDetailsRequestChangePublicationStatusParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Configure the publication status of a file and its versions. +// +// The property IsPublished is required. +type UpdateFileDetailsRequestChangePublicationStatusPublishParam struct { + // Set to `true` to publish the file. Set to `false` to unpublish the file. + IsPublished bool `json:"isPublished,required"` + // Set to `true` to publish/unpublish all versions of the file. Set to `false` to + // publish/unpublish only the current version of the file. + IncludeFileVersions param.Opt[bool] `json:"includeFileVersions,omitzero"` + paramObj +} + +func (r UpdateFileDetailsRequestChangePublicationStatusPublishParam) MarshalJSON() (data []byte, err error) { + type shadow UpdateFileDetailsRequestChangePublicationStatusPublishParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *UpdateFileDetailsRequestChangePublicationStatusPublishParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Object containing details of a file or file version. +type FileUpdateResponse struct { + ExtensionStatus FileUpdateResponseExtensionStatus `json:"extensionStatus"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ExtensionStatus respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` + File +} + +// Returns the unmodified JSON received from the API +func (r FileUpdateResponse) RawJSON() string { return r.JSON.raw } +func (r *FileUpdateResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type FileUpdateResponseExtensionStatus struct { + // Any of "success", "pending", "failed". + AIAutoDescription string `json:"ai-auto-description"` + // Any of "success", "pending", "failed". + AwsAutoTagging string `json:"aws-auto-tagging"` + // Any of "success", "pending", "failed". + GoogleAutoTagging string `json:"google-auto-tagging"` + // Any of "success", "pending", "failed". + RemoveBg string `json:"remove-bg"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + AIAutoDescription respjson.Field + AwsAutoTagging respjson.Field + GoogleAutoTagging respjson.Field + RemoveBg respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r FileUpdateResponseExtensionStatus) RawJSON() string { return r.JSON.raw } +func (r *FileUpdateResponseExtensionStatus) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type FileCopyResponse struct { + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r FileCopyResponse) RawJSON() string { return r.JSON.raw } +func (r *FileCopyResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type FileMoveResponse struct { + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r FileMoveResponse) RawJSON() string { return r.JSON.raw } +func (r *FileMoveResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type FileRenameResponse struct { + // Unique identifier of the purge request. This can be used to check the status of + // the purge request. + PurgeRequestID string `json:"purgeRequestId"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + PurgeRequestID respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r FileRenameResponse) RawJSON() string { return r.JSON.raw } +func (r *FileRenameResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Object containing details of a successful upload. +type FileUploadResponse struct { + // An array of tags assigned to the uploaded file by auto tagging. + AITags []FileUploadResponseAITag `json:"AITags,nullable"` + // The audio codec used in the video (only for video). + AudioCodec string `json:"audioCodec"` + // The bit rate of the video in kbps (only for video). + BitRate int64 `json:"bitRate"` + // Value of custom coordinates associated with the image in the format + // `x,y,width,height`. If `customCoordinates` are not defined, then it is `null`. + // Send `customCoordinates` in `responseFields` in API request to get the value of + // this field. + CustomCoordinates string `json:"customCoordinates,nullable"` + // A key-value data associated with the asset. Use `responseField` in API request + // to get `customMetadata` in the upload API response. Before setting any custom + // metadata on an asset, you have to create the field using custom metadata fields + // API. Send `customMetadata` in `responseFields` in API request to get the value + // of this field. + CustomMetadata map[string]any `json:"customMetadata"` + // Optional text to describe the contents of the file. Can be set by the user or + // the ai-auto-description extension. + Description string `json:"description"` + // The duration of the video in seconds (only for video). + Duration int64 `json:"duration"` + // Consolidated embedded metadata associated with the file. It includes exif, iptc, + // and xmp data. Send `embeddedMetadata` in `responseFields` in API request to get + // embeddedMetadata in the upload API response. + EmbeddedMetadata map[string]any `json:"embeddedMetadata"` + // Extension names with their processing status at the time of completion of the + // request. It could have one of the following status values: + // + // `success`: The extension has been successfully applied. `failed`: The extension + // has failed and will not be retried. `pending`: The extension will finish + // processing in some time. On completion, the final status (success / failed) will + // be sent to the `webhookUrl` provided. + // + // If no extension was requested, then this parameter is not returned. + ExtensionStatus FileUploadResponseExtensionStatus `json:"extensionStatus"` + // Unique fileId. Store this fileld in your database, as this will be used to + // perform update action on this file. + FileID string `json:"fileId"` + // The relative path of the file in the media library e.g. + // `/marketing-assets/new-banner.jpg`. + FilePath string `json:"filePath"` + // Type of the uploaded file. Possible values are `image`, `non-image`. + FileType string `json:"fileType"` + // Height of the image in pixels (Only for images) + Height float64 `json:"height"` + // Is the file marked as private. It can be either `true` or `false`. Send + // `isPrivateFile` in `responseFields` in API request to get the value of this + // field. + IsPrivateFile bool `json:"isPrivateFile"` + // Is the file published or in draft state. It can be either `true` or `false`. + // Send `isPublished` in `responseFields` in API request to get the value of this + // field. + IsPublished bool `json:"isPublished"` + // Legacy metadata. Send `metadata` in `responseFields` in API request to get + // metadata in the upload API response. + Metadata Metadata `json:"metadata"` + // Name of the asset. + Name string `json:"name"` + // Size of the image file in Bytes. + Size float64 `json:"size"` + // The array of tags associated with the asset. If no tags are set, it will be + // `null`. Send `tags` in `responseFields` in API request to get the value of this + // field. + Tags []string `json:"tags,nullable"` + // In the case of an image, a small thumbnail URL. + ThumbnailURL string `json:"thumbnailUrl"` + // A publicly accessible URL of the file. + URL string `json:"url"` + // An object containing the file or file version's `id` (versionId) and `name`. + VersionInfo FileUploadResponseVersionInfo `json:"versionInfo"` + // The video codec used in the video (only for video). + VideoCodec string `json:"videoCodec"` + // Width of the image in pixels (Only for Images) + Width float64 `json:"width"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + AITags respjson.Field + AudioCodec respjson.Field + BitRate respjson.Field + CustomCoordinates respjson.Field + CustomMetadata respjson.Field + Description respjson.Field + Duration respjson.Field + EmbeddedMetadata respjson.Field + ExtensionStatus respjson.Field + FileID respjson.Field + FilePath respjson.Field + FileType respjson.Field + Height respjson.Field + IsPrivateFile respjson.Field + IsPublished respjson.Field + Metadata respjson.Field + Name respjson.Field + Size respjson.Field + Tags respjson.Field + ThumbnailURL respjson.Field + URL respjson.Field + VersionInfo respjson.Field + VideoCodec respjson.Field + Width respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r FileUploadResponse) RawJSON() string { return r.JSON.raw } +func (r *FileUploadResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type FileUploadResponseAITag struct { + // Confidence score of the tag. + Confidence float64 `json:"confidence"` + // Name of the tag. + Name string `json:"name"` + // Array of `AITags` associated with the image. If no `AITags` are set, it will be + // null. These tags can be added using the `google-auto-tagging` or + // `aws-auto-tagging` extensions. + Source string `json:"source"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Confidence respjson.Field + Name respjson.Field + Source respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r FileUploadResponseAITag) RawJSON() string { return r.JSON.raw } +func (r *FileUploadResponseAITag) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Extension names with their processing status at the time of completion of the +// request. It could have one of the following status values: +// +// `success`: The extension has been successfully applied. `failed`: The extension +// has failed and will not be retried. `pending`: The extension will finish +// processing in some time. On completion, the final status (success / failed) will +// be sent to the `webhookUrl` provided. +// +// If no extension was requested, then this parameter is not returned. +type FileUploadResponseExtensionStatus struct { + // Any of "success", "pending", "failed". + AIAutoDescription string `json:"ai-auto-description"` + // Any of "success", "pending", "failed". + AwsAutoTagging string `json:"aws-auto-tagging"` + // Any of "success", "pending", "failed". + GoogleAutoTagging string `json:"google-auto-tagging"` + // Any of "success", "pending", "failed". + RemoveBg string `json:"remove-bg"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + AIAutoDescription respjson.Field + AwsAutoTagging respjson.Field + GoogleAutoTagging respjson.Field + RemoveBg respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r FileUploadResponseExtensionStatus) RawJSON() string { return r.JSON.raw } +func (r *FileUploadResponseExtensionStatus) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// An object containing the file or file version's `id` (versionId) and `name`. +type FileUploadResponseVersionInfo struct { + // Unique identifier of the file version. + ID string `json:"id"` + // Name of the file version. + Name string `json:"name"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ID respjson.Field + Name respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r FileUploadResponseVersionInfo) RawJSON() string { return r.JSON.raw } +func (r *FileUploadResponseVersionInfo) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type FileUpdateParams struct { + UpdateFileDetailsRequest UpdateFileDetailsRequestUnionParam + paramObj +} + +func (r FileUpdateParams) MarshalJSON() (data []byte, err error) { + return shimjson.Marshal(r.UpdateFileDetailsRequest) +} +func (r *FileUpdateParams) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &r.UpdateFileDetailsRequest) +} + +type FileCopyParams struct { + // Full path to the folder you want to copy the above file into. + DestinationPath string `json:"destinationPath,required"` + // The full path of the file you want to copy. + SourceFilePath string `json:"sourceFilePath,required"` + // Option to copy all versions of a file. By default, only the current version of + // the file is copied. When set to true, all versions of the file will be copied. + // Default value - `false`. + IncludeFileVersions param.Opt[bool] `json:"includeFileVersions,omitzero"` + paramObj +} + +func (r FileCopyParams) MarshalJSON() (data []byte, err error) { + type shadow FileCopyParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *FileCopyParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type FileMoveParams struct { + // Full path to the folder you want to move the above file into. + DestinationPath string `json:"destinationPath,required"` + // The full path of the file you want to move. + SourceFilePath string `json:"sourceFilePath,required"` + paramObj +} + +func (r FileMoveParams) MarshalJSON() (data []byte, err error) { + type shadow FileMoveParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *FileMoveParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type FileRenameParams struct { + // The full path of the file you want to rename. + FilePath string `json:"filePath,required"` + // The new name of the file. A filename can contain: + // + // Alphanumeric Characters: `a-z`, `A-Z`, `0-9` (including Unicode letters, marks, + // and numerals in other languages). Special Characters: `.`, `_`, and `-`. + // + // Any other character, including space, will be replaced by `_`. + NewFileName string `json:"newFileName,required"` + // Option to purge cache for the old file and its versions' URLs. + // + // When set to true, it will internally issue a purge cache request on CDN to + // remove cached content of old file and its versions. This purge request is + // counted against your monthly purge quota. + // + // Note: If the old file were accessible at + // `https://ik.imagekit.io/demo/old-filename.jpg`, a purge cache request would be + // issued against `https://ik.imagekit.io/demo/old-filename.jpg*` (with a wildcard + // at the end). It will remove the file and its versions' URLs and any + // transformations made using query parameters on this file or its versions. + // However, the cache for file transformations made using path parameters will + // persist. You can purge them using the purge API. For more details, refer to the + // purge API documentation. + // + // Default value - `false` + PurgeCache param.Opt[bool] `json:"purgeCache,omitzero"` + paramObj +} + +func (r FileRenameParams) MarshalJSON() (data []byte, err error) { + type shadow FileRenameParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *FileRenameParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type FileUploadParams struct { + // The API accepts any of the following: + // + // - **Binary data** – send the raw bytes as `multipart/form-data`. + // - **HTTP / HTTPS URL** – a publicly reachable URL that ImageKit’s servers can + // fetch. + // - **Base64 string** – the file encoded as a Base64 data URI or plain Base64. + // + // When supplying a URL, the server must receive the response headers within 8 + // seconds; otherwise the request fails with 400 Bad Request. + File io.Reader `json:"file,omitzero,required" format:"binary"` + // The name with which the file has to be uploaded. The file name can contain: + // + // - Alphanumeric Characters: `a-z`, `A-Z`, `0-9`. + // - Special Characters: `.`, `-` + // + // Any other character including space will be replaced by `_` + FileName string `json:"fileName,required"` + // A unique value that the ImageKit.io server will use to recognize and prevent + // subsequent retries for the same request. We suggest using V4 UUIDs, or another + // random string with enough entropy to avoid collisions. This field is only + // required for authentication when uploading a file from the client side. + // + // **Note**: Sending a value that has been used in the past will result in a + // validation error. Even if your previous request resulted in an error, you should + // always send a new value for this field. + Token param.Opt[string] `json:"token,omitzero"` + // Server-side checks to run on the asset. Read more about + // [Upload API checks](/docs/api-reference/upload-file/upload-file#upload-api-checks). + Checks param.Opt[string] `json:"checks,omitzero"` + // Define an important area in the image. This is only relevant for image type + // files. + // + // - To be passed as a string with the x and y coordinates of the top-left corner, + // and width and height of the area of interest in the format `x,y,width,height`. + // For example - `10,10,100,100` + // - Can be used with fo-customtransformation. + // - If this field is not specified and the file is overwritten, then + // customCoordinates will be removed. + CustomCoordinates param.Opt[string] `json:"customCoordinates,omitzero"` + // Optional text to describe the contents of the file. + Description param.Opt[string] `json:"description,omitzero"` + // The time until your signature is valid. It must be a + // [Unix time](https://en.wikipedia.org/wiki/Unix_time) in less than 1 hour into + // the future. It should be in seconds. This field is only required for + // authentication when uploading a file from the client side. + Expire param.Opt[int64] `json:"expire,omitzero"` + // The folder path in which the image has to be uploaded. If the folder(s) didn't + // exist before, a new folder(s) is created. + // + // The folder name can contain: + // + // - Alphanumeric Characters: `a-z` , `A-Z` , `0-9` + // - Special Characters: `/` , `_` , `-` + // + // Using multiple `/` creates a nested folder. + Folder param.Opt[string] `json:"folder,omitzero"` + // Whether to mark the file as private or not. + // + // If `true`, the file is marked as private and is accessible only using named + // transformation or signed URL. + IsPrivateFile param.Opt[bool] `json:"isPrivateFile,omitzero"` + // Whether to upload file as published or not. + // + // If `false`, the file is marked as unpublished, which restricts access to the + // file only via the media library. Files in draft or unpublished state can only be + // publicly accessed after being published. + // + // The option to upload in draft state is only available in custom enterprise + // pricing plans. + IsPublished param.Opt[bool] `json:"isPublished,omitzero"` + // If set to `true` and a file already exists at the exact location, its AITags + // will be removed. Set `overwriteAITags` to `false` to preserve AITags. + OverwriteAITags param.Opt[bool] `json:"overwriteAITags,omitzero"` + // If the request does not have `customMetadata`, and a file already exists at the + // exact location, existing customMetadata will be removed. + OverwriteCustomMetadata param.Opt[bool] `json:"overwriteCustomMetadata,omitzero"` + // If `false` and `useUniqueFileName` is also `false`, and a file already exists at + // the exact location, upload API will return an error immediately. + OverwriteFile param.Opt[bool] `json:"overwriteFile,omitzero"` + // If the request does not have `tags`, and a file already exists at the exact + // location, existing tags will be removed. + OverwriteTags param.Opt[bool] `json:"overwriteTags,omitzero"` + // Your ImageKit.io public key. This field is only required for authentication when + // uploading a file from the client side. + PublicKey param.Opt[string] `json:"publicKey,omitzero"` + // HMAC-SHA1 digest of the token+expire using your ImageKit.io private API key as a + // key. Learn how to create a signature on the page below. This should be in + // lowercase. + // + // Signature must be calculated on the server-side. This field is only required for + // authentication when uploading a file from the client side. + Signature param.Opt[string] `json:"signature,omitzero"` + // Whether to use a unique filename for this file or not. + // + // If `true`, ImageKit.io will add a unique suffix to the filename parameter to get + // a unique filename. + // + // If `false`, then the image is uploaded with the provided filename parameter, and + // any existing file with the same name is replaced. + UseUniqueFileName param.Opt[bool] `json:"useUniqueFileName,omitzero"` + // The final status of extensions after they have completed execution will be + // delivered to this endpoint as a POST request. + // [Learn more](/docs/api-reference/digital-asset-management-dam/managing-assets/update-file-details#webhook-payload-structure) + // about the webhook payload structure. + WebhookURL param.Opt[string] `json:"webhookUrl,omitzero" format:"uri"` + // JSON key-value pairs to associate with the asset. Create the custom metadata + // fields before setting these values. + CustomMetadata map[string]any `json:"customMetadata,omitzero"` + // Array of extensions to be applied to the asset. Each extension can be configured + // with specific parameters based on the extension type. + Extensions shared.ExtensionsParam `json:"extensions,omitzero"` + // Array of response field keys to include in the API response body. + // + // Any of "tags", "customCoordinates", "isPrivateFile", "embeddedMetadata", + // "isPublished", "customMetadata", "metadata". + ResponseFields []string `json:"responseFields,omitzero"` + // Set the tags while uploading the file. Provide an array of tag strings (e.g. + // `["tag1", "tag2", "tag3"]`). The combined length of all tag characters must not + // exceed 500, and the `%` character is not allowed. If this field is not specified + // and the file is overwritten, the existing tags will be removed. + Tags []string `json:"tags,omitzero"` + // Configure pre-processing (`pre`) and post-processing (`post`) transformations. + // + // - `pre` — applied before the file is uploaded to the Media Library. + // Useful for reducing file size or applying basic optimizations upfront (e.g., + // resize, compress). + // + // - `post` — applied immediately after upload. + // Ideal for generating transformed versions (like video encodes or thumbnails) + // in advance, so they're ready for delivery without delay. + // + // You can mix and match any combination of post-processing types. + Transformation FileUploadParamsTransformation `json:"transformation,omitzero"` + paramObj +} + +func (r FileUploadParams) MarshalMultipart() (data []byte, contentType string, err error) { + buf := bytes.NewBuffer(nil) + writer := multipart.NewWriter(buf) + err = apiform.MarshalRoot(r, writer) + if err == nil { + err = apiform.WriteExtras(writer, r.ExtraFields()) + } + if err != nil { + writer.Close() + return nil, "", err + } + err = writer.Close() + if err != nil { + return nil, "", err + } + return buf.Bytes(), writer.FormDataContentType(), nil +} + +// Configure pre-processing (`pre`) and post-processing (`post`) transformations. +// +// - `pre` — applied before the file is uploaded to the Media Library. +// Useful for reducing file size or applying basic optimizations upfront (e.g., +// resize, compress). +// +// - `post` — applied immediately after upload. +// Ideal for generating transformed versions (like video encodes or thumbnails) +// in advance, so they're ready for delivery without delay. +// +// You can mix and match any combination of post-processing types. +type FileUploadParamsTransformation struct { + // Transformation string to apply before uploading the file to the Media Library. + // Useful for optimizing files at ingestion. + Pre param.Opt[string] `json:"pre,omitzero"` + // List of transformations to apply _after_ the file is uploaded. + // Each item must match one of the following types: `transformation`, + // `gif-to-video`, `thumbnail`, `abs`. + Post []FileUploadParamsTransformationPostUnion `json:"post,omitzero"` + paramObj +} + +func (r FileUploadParamsTransformation) MarshalJSON() (data []byte, err error) { + type shadow FileUploadParamsTransformation + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *FileUploadParamsTransformation) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Only one field can be non-zero. +// +// Use [param.IsOmitted] to confirm if a field is set. +type FileUploadParamsTransformationPostUnion struct { + OfTransformation *FileUploadParamsTransformationPostTransformation `json:",omitzero,inline"` + OfGifToVideo *FileUploadParamsTransformationPostGifToVideo `json:",omitzero,inline"` + OfThumbnail *FileUploadParamsTransformationPostThumbnail `json:",omitzero,inline"` + OfAbs *FileUploadParamsTransformationPostAbs `json:",omitzero,inline"` + paramUnion +} + +func (u FileUploadParamsTransformationPostUnion) MarshalJSON() ([]byte, error) { + return param.MarshalUnion(u, u.OfTransformation, u.OfGifToVideo, u.OfThumbnail, u.OfAbs) +} +func (u *FileUploadParamsTransformationPostUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, u) +} + +func (u *FileUploadParamsTransformationPostUnion) asAny() any { + if !param.IsOmitted(u.OfTransformation) { + return u.OfTransformation + } else if !param.IsOmitted(u.OfGifToVideo) { + return u.OfGifToVideo + } else if !param.IsOmitted(u.OfThumbnail) { + return u.OfThumbnail + } else if !param.IsOmitted(u.OfAbs) { + return u.OfAbs + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u FileUploadParamsTransformationPostUnion) GetProtocol() *string { + if vt := u.OfAbs; vt != nil { + return &vt.Protocol + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u FileUploadParamsTransformationPostUnion) GetType() *string { + if vt := u.OfTransformation; vt != nil { + return (*string)(&vt.Type) + } else if vt := u.OfGifToVideo; vt != nil { + return (*string)(&vt.Type) + } else if vt := u.OfThumbnail; vt != nil { + return (*string)(&vt.Type) + } else if vt := u.OfAbs; vt != nil { + return (*string)(&vt.Type) + } + return nil +} + +// Returns a pointer to the underlying variant's property, if present. +func (u FileUploadParamsTransformationPostUnion) GetValue() *string { + if vt := u.OfTransformation; vt != nil { + return (*string)(&vt.Value) + } else if vt := u.OfGifToVideo; vt != nil && vt.Value.Valid() { + return &vt.Value.Value + } else if vt := u.OfThumbnail; vt != nil && vt.Value.Valid() { + return &vt.Value.Value + } else if vt := u.OfAbs; vt != nil { + return (*string)(&vt.Value) + } + return nil +} + +func init() { + apijson.RegisterUnion[FileUploadParamsTransformationPostUnion]( + "type", + apijson.Discriminator[FileUploadParamsTransformationPostTransformation]("transformation"), + apijson.Discriminator[FileUploadParamsTransformationPostGifToVideo]("gif-to-video"), + apijson.Discriminator[FileUploadParamsTransformationPostThumbnail]("thumbnail"), + apijson.Discriminator[FileUploadParamsTransformationPostAbs]("abs"), + ) +} + +// The properties Type, Value are required. +type FileUploadParamsTransformationPostTransformation struct { + // Transformation string (e.g. `w-200,h-200`). + // Same syntax as ImageKit URL-based transformations. + Value string `json:"value,required"` + // Transformation type. + // + // This field can be elided, and will marshal its zero value as "transformation". + Type constant.Transformation `json:"type,required"` + paramObj +} + +func (r FileUploadParamsTransformationPostTransformation) MarshalJSON() (data []byte, err error) { + type shadow FileUploadParamsTransformationPostTransformation + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *FileUploadParamsTransformationPostTransformation) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// The property Type is required. +type FileUploadParamsTransformationPostGifToVideo struct { + // Optional transformation string to apply to the output video. + // **Example**: `q-80` + Value param.Opt[string] `json:"value,omitzero"` + // Converts an animated GIF into an MP4. + // + // This field can be elided, and will marshal its zero value as "gif-to-video". + Type constant.GifToVideo `json:"type,required"` + paramObj +} + +func (r FileUploadParamsTransformationPostGifToVideo) MarshalJSON() (data []byte, err error) { + type shadow FileUploadParamsTransformationPostGifToVideo + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *FileUploadParamsTransformationPostGifToVideo) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// The property Type is required. +type FileUploadParamsTransformationPostThumbnail struct { + // Optional transformation string. + // **Example**: `w-150,h-150` + Value param.Opt[string] `json:"value,omitzero"` + // Generates a thumbnail image. + // + // This field can be elided, and will marshal its zero value as "thumbnail". + Type constant.Thumbnail `json:"type,required"` + paramObj +} + +func (r FileUploadParamsTransformationPostThumbnail) MarshalJSON() (data []byte, err error) { + type shadow FileUploadParamsTransformationPostThumbnail + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *FileUploadParamsTransformationPostThumbnail) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// The properties Protocol, Type, Value are required. +type FileUploadParamsTransformationPostAbs struct { + // Streaming protocol to use (`hls` or `dash`). + // + // Any of "hls", "dash". + Protocol string `json:"protocol,omitzero,required"` + // List of different representations you want to create separated by an underscore. + Value string `json:"value,required"` + // Adaptive Bitrate Streaming (ABS) setup. + // + // This field can be elided, and will marshal its zero value as "abs". + Type constant.Abs `json:"type,required"` + paramObj +} + +func (r FileUploadParamsTransformationPostAbs) MarshalJSON() (data []byte, err error) { + type shadow FileUploadParamsTransformationPostAbs + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *FileUploadParamsTransformationPostAbs) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +func init() { + apijson.RegisterFieldValidator[FileUploadParamsTransformationPostAbs]( + "protocol", "hls", "dash", + ) +} diff --git a/file_test.go b/file_test.go new file mode 100644 index 0000000..b4a8611 --- /dev/null +++ b/file_test.go @@ -0,0 +1,296 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit_test + +import ( + "bytes" + "context" + "errors" + "io" + "os" + "testing" + + "github.com/imagekit-developer/imagekit-go" + "github.com/imagekit-developer/imagekit-go/internal/testutil" + "github.com/imagekit-developer/imagekit-go/option" + "github.com/imagekit-developer/imagekit-go/shared" +) + +func TestFileUpdateWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Files.Update( + context.TODO(), + "fileId", + imagekit.FileUpdateParams{ + UpdateFileDetailsRequest: imagekit.UpdateFileDetailsRequestUnionParam{ + OfUpdateFileDetails: &imagekit.UpdateFileDetailsRequestUpdateFileDetailsParam{ + CustomCoordinates: imagekit.String("10,10,100,100"), + CustomMetadata: map[string]any{ + "brand": "bar", + "color": "bar", + }, + Description: imagekit.String("description"), + Extensions: shared.ExtensionsParam{shared.ExtensionUnionParam{ + OfRemoveBg: &shared.ExtensionRemoveBgParam{ + Options: shared.ExtensionRemoveBgOptionsParam{ + AddShadow: imagekit.Bool(true), + BgColor: imagekit.String("bg_color"), + BgImageURL: imagekit.String("bg_image_url"), + Semitransparency: imagekit.Bool(true), + }, + }, + }, shared.ExtensionUnionParam{ + OfAutoTagging: &shared.ExtensionAutoTaggingParam{ + MaxTags: 10, + MinConfidence: 80, + Name: "google-auto-tagging", + }, + }, shared.ExtensionUnionParam{ + OfAutoTagging: &shared.ExtensionAutoTaggingParam{ + MaxTags: 10, + MinConfidence: 80, + Name: "aws-auto-tagging", + }, + }, shared.ExtensionUnionParam{ + OfAIAutoDescription: &shared.ExtensionAIAutoDescriptionParam{}, + }}, + RemoveAITags: imagekit.UpdateFileDetailsRequestUpdateFileDetailsRemoveAITagsUnionParam{ + OfStringArray: []string{"car", "vehicle", "motorsports"}, + }, + Tags: []string{"tag1", "tag2"}, + WebhookURL: imagekit.String("https://webhook.site/0d6b6c7a-8e5a-4b3a-8b7c-0d6b6c7a8e5a"), + }, + }, + }, + ) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestFileDelete(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + err := client.Files.Delete(context.TODO(), "fileId") + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestFileCopyWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Files.Copy(context.TODO(), imagekit.FileCopyParams{ + DestinationPath: "/folder/to/copy/into/", + SourceFilePath: "/path/to/file.jpg", + IncludeFileVersions: imagekit.Bool(false), + }) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestFileGet(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Files.Get(context.TODO(), "fileId") + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestFileMove(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Files.Move(context.TODO(), imagekit.FileMoveParams{ + DestinationPath: "/folder/to/move/into/", + SourceFilePath: "/path/to/file.jpg", + }) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestFileRenameWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Files.Rename(context.TODO(), imagekit.FileRenameParams{ + FilePath: "/path/to/file.jpg", + NewFileName: "newFileName.jpg", + PurgeCache: imagekit.Bool(true), + }) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestFileUploadWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Files.Upload(context.TODO(), imagekit.FileUploadParams{ + File: io.Reader(bytes.NewBuffer([]byte("some file contents"))), + FileName: "fileName", + Token: imagekit.String("token"), + Checks: imagekit.String("\"request.folder\" : \"marketing/\"\n"), + CustomCoordinates: imagekit.String("customCoordinates"), + CustomMetadata: map[string]any{ + "brand": "bar", + "color": "bar", + }, + Description: imagekit.String("Running shoes"), + Expire: imagekit.Int(0), + Extensions: shared.ExtensionsParam{shared.ExtensionUnionParam{ + OfRemoveBg: &shared.ExtensionRemoveBgParam{ + Options: shared.ExtensionRemoveBgOptionsParam{ + AddShadow: imagekit.Bool(true), + BgColor: imagekit.String("bg_color"), + BgImageURL: imagekit.String("bg_image_url"), + Semitransparency: imagekit.Bool(true), + }, + }, + }, shared.ExtensionUnionParam{ + OfAutoTagging: &shared.ExtensionAutoTaggingParam{ + MaxTags: 5, + MinConfidence: 95, + Name: "google-auto-tagging", + }, + }, shared.ExtensionUnionParam{ + OfAIAutoDescription: &shared.ExtensionAIAutoDescriptionParam{}, + }}, + Folder: imagekit.String("folder"), + IsPrivateFile: imagekit.Bool(true), + IsPublished: imagekit.Bool(true), + OverwriteAITags: imagekit.Bool(true), + OverwriteCustomMetadata: imagekit.Bool(true), + OverwriteFile: imagekit.Bool(true), + OverwriteTags: imagekit.Bool(true), + PublicKey: imagekit.String("publicKey"), + ResponseFields: []string{"tags", "customCoordinates", "isPrivateFile"}, + Signature: imagekit.String("signature"), + Tags: []string{"t-shirt", "round-neck", "men"}, + Transformation: imagekit.FileUploadParamsTransformation{ + Post: []imagekit.FileUploadParamsTransformationPostUnion{{ + OfThumbnail: &imagekit.FileUploadParamsTransformationPostThumbnail{ + Value: imagekit.String("w-150,h-150"), + }, + }, { + OfAbs: &imagekit.FileUploadParamsTransformationPostAbs{ + Protocol: "dash", + Value: "sr-240_360_480_720_1080", + }, + }}, + Pre: imagekit.String("w-300,h-300,q-80"), + }, + UseUniqueFileName: imagekit.Bool(true), + WebhookURL: imagekit.String("https://example.com"), + }) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/filebulk.go b/filebulk.go new file mode 100644 index 0000000..89fbf13 --- /dev/null +++ b/filebulk.go @@ -0,0 +1,204 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit + +import ( + "context" + "net/http" + + "github.com/imagekit-developer/imagekit-go/internal/apijson" + "github.com/imagekit-developer/imagekit-go/internal/requestconfig" + "github.com/imagekit-developer/imagekit-go/option" + "github.com/imagekit-developer/imagekit-go/packages/param" + "github.com/imagekit-developer/imagekit-go/packages/respjson" +) + +// FileBulkService contains methods and other services that help with interacting +// with the ImageKit API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewFileBulkService] method instead. +type FileBulkService struct { + Options []option.RequestOption +} + +// NewFileBulkService generates a new service that applies the given options to +// each request. These options are applied after the parent client's options (if +// there is one), and before any request-specific options. +func NewFileBulkService(opts ...option.RequestOption) (r FileBulkService) { + r = FileBulkService{} + r.Options = opts + return +} + +// This API deletes multiple files and all their file versions permanently. +// +// Note: If a file or specific transformation has been requested in the past, then +// the response is cached. Deleting a file does not purge the cache. You can purge +// the cache using purge cache API. +// +// A maximum of 100 files can be deleted at a time. +func (r *FileBulkService) Delete(ctx context.Context, body FileBulkDeleteParams, opts ...option.RequestOption) (res *FileBulkDeleteResponse, err error) { + opts = append(r.Options[:], opts...) + path := "v1/files/batch/deleteByFileIds" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +// This API adds tags to multiple files in bulk. A maximum of 50 files can be +// specified at a time. +func (r *FileBulkService) AddTags(ctx context.Context, body FileBulkAddTagsParams, opts ...option.RequestOption) (res *FileBulkAddTagsResponse, err error) { + opts = append(r.Options[:], opts...) + path := "v1/files/addTags" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +// This API removes AITags from multiple files in bulk. A maximum of 50 files can +// be specified at a time. +func (r *FileBulkService) RemoveAITags(ctx context.Context, body FileBulkRemoveAITagsParams, opts ...option.RequestOption) (res *FileBulkRemoveAITagsResponse, err error) { + opts = append(r.Options[:], opts...) + path := "v1/files/removeAITags" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +// This API removes tags from multiple files in bulk. A maximum of 50 files can be +// specified at a time. +func (r *FileBulkService) RemoveTags(ctx context.Context, body FileBulkRemoveTagsParams, opts ...option.RequestOption) (res *FileBulkRemoveTagsResponse, err error) { + opts = append(r.Options[:], opts...) + path := "v1/files/removeTags" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +type FileBulkDeleteResponse struct { + // An array of fileIds that were successfully deleted. + SuccessfullyDeletedFileIDs []string `json:"successfullyDeletedFileIds"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + SuccessfullyDeletedFileIDs respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r FileBulkDeleteResponse) RawJSON() string { return r.JSON.raw } +func (r *FileBulkDeleteResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type FileBulkAddTagsResponse struct { + // An array of fileIds that in which tags were successfully added. + SuccessfullyUpdatedFileIDs []string `json:"successfullyUpdatedFileIds"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + SuccessfullyUpdatedFileIDs respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r FileBulkAddTagsResponse) RawJSON() string { return r.JSON.raw } +func (r *FileBulkAddTagsResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type FileBulkRemoveAITagsResponse struct { + // An array of fileIds that in which AITags were successfully removed. + SuccessfullyUpdatedFileIDs []string `json:"successfullyUpdatedFileIds"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + SuccessfullyUpdatedFileIDs respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r FileBulkRemoveAITagsResponse) RawJSON() string { return r.JSON.raw } +func (r *FileBulkRemoveAITagsResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type FileBulkRemoveTagsResponse struct { + // An array of fileIds that in which tags were successfully removed. + SuccessfullyUpdatedFileIDs []string `json:"successfullyUpdatedFileIds"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + SuccessfullyUpdatedFileIDs respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r FileBulkRemoveTagsResponse) RawJSON() string { return r.JSON.raw } +func (r *FileBulkRemoveTagsResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type FileBulkDeleteParams struct { + // An array of fileIds which you want to delete. + FileIDs []string `json:"fileIds,omitzero,required"` + paramObj +} + +func (r FileBulkDeleteParams) MarshalJSON() (data []byte, err error) { + type shadow FileBulkDeleteParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *FileBulkDeleteParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type FileBulkAddTagsParams struct { + // An array of fileIds to which you want to add tags. + FileIDs []string `json:"fileIds,omitzero,required"` + // An array of tags that you want to add to the files. + Tags []string `json:"tags,omitzero,required"` + paramObj +} + +func (r FileBulkAddTagsParams) MarshalJSON() (data []byte, err error) { + type shadow FileBulkAddTagsParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *FileBulkAddTagsParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type FileBulkRemoveAITagsParams struct { + // An array of AITags that you want to remove from the files. + AITags []string `json:"AITags,omitzero,required"` + // An array of fileIds from which you want to remove AITags. + FileIDs []string `json:"fileIds,omitzero,required"` + paramObj +} + +func (r FileBulkRemoveAITagsParams) MarshalJSON() (data []byte, err error) { + type shadow FileBulkRemoveAITagsParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *FileBulkRemoveAITagsParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type FileBulkRemoveTagsParams struct { + // An array of fileIds from which you want to remove tags. + FileIDs []string `json:"fileIds,omitzero,required"` + // An array of tags that you want to remove from the files. + Tags []string `json:"tags,omitzero,required"` + paramObj +} + +func (r FileBulkRemoveTagsParams) MarshalJSON() (data []byte, err error) { + type shadow FileBulkRemoveTagsParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *FileBulkRemoveTagsParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} diff --git a/filebulk_test.go b/filebulk_test.go new file mode 100644 index 0000000..8174d09 --- /dev/null +++ b/filebulk_test.go @@ -0,0 +1,121 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/imagekit-developer/imagekit-go" + "github.com/imagekit-developer/imagekit-go/internal/testutil" + "github.com/imagekit-developer/imagekit-go/option" +) + +func TestFileBulkDelete(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Files.Bulk.Delete(context.TODO(), imagekit.FileBulkDeleteParams{ + FileIDs: []string{"598821f949c0a938d57563bd", "598821f949c0a938d57563be"}, + }) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestFileBulkAddTags(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Files.Bulk.AddTags(context.TODO(), imagekit.FileBulkAddTagsParams{ + FileIDs: []string{"598821f949c0a938d57563bd", "598821f949c0a938d57563be"}, + Tags: []string{"t-shirt", "round-neck", "sale2019"}, + }) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestFileBulkRemoveAITags(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Files.Bulk.RemoveAITags(context.TODO(), imagekit.FileBulkRemoveAITagsParams{ + AITags: []string{"t-shirt", "round-neck", "sale2019"}, + FileIDs: []string{"598821f949c0a938d57563bd", "598821f949c0a938d57563be"}, + }) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestFileBulkRemoveTags(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Files.Bulk.RemoveTags(context.TODO(), imagekit.FileBulkRemoveTagsParams{ + FileIDs: []string{"598821f949c0a938d57563bd", "598821f949c0a938d57563be"}, + Tags: []string{"t-shirt", "round-neck", "sale2019"}, + }) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/filemetadata.go b/filemetadata.go new file mode 100644 index 0000000..7f37e72 --- /dev/null +++ b/filemetadata.go @@ -0,0 +1,75 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + + "github.com/imagekit-developer/imagekit-go/internal/apiquery" + "github.com/imagekit-developer/imagekit-go/internal/requestconfig" + "github.com/imagekit-developer/imagekit-go/option" +) + +// FileMetadataService contains methods and other services that help with +// interacting with the ImageKit API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewFileMetadataService] method instead. +type FileMetadataService struct { + Options []option.RequestOption +} + +// NewFileMetadataService generates a new service that applies the given options to +// each request. These options are applied after the parent client's options (if +// there is one), and before any request-specific options. +func NewFileMetadataService(opts ...option.RequestOption) (r FileMetadataService) { + r = FileMetadataService{} + r.Options = opts + return +} + +// You can programmatically get image EXIF, pHash, and other metadata for uploaded +// files in the ImageKit.io media library using this API. +// +// You can also get the metadata in upload API response by passing `metadata` in +// `responseFields` parameter. +func (r *FileMetadataService) Get(ctx context.Context, fileID string, opts ...option.RequestOption) (res *Metadata, err error) { + opts = append(r.Options[:], opts...) + if fileID == "" { + err = errors.New("missing required fileId parameter") + return + } + path := fmt.Sprintf("v1/files/%s/metadata", fileID) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return +} + +// Get image EXIF, pHash, and other metadata from ImageKit.io powered remote URL +// using this API. +func (r *FileMetadataService) GetFromURL(ctx context.Context, query FileMetadataGetFromURLParams, opts ...option.RequestOption) (res *Metadata, err error) { + opts = append(r.Options[:], opts...) + path := "v1/files/metadata" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) + return +} + +type FileMetadataGetFromURLParams struct { + // Should be a valid file URL. It should be accessible using your ImageKit.io + // account. + URL string `query:"url,required" format:"uri" json:"-"` + paramObj +} + +// URLQuery serializes [FileMetadataGetFromURLParams]'s query parameters as +// `url.Values`. +func (r FileMetadataGetFromURLParams) URLQuery() (v url.Values, err error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} diff --git a/filemetadata_test.go b/filemetadata_test.go new file mode 100644 index 0000000..ac9cad0 --- /dev/null +++ b/filemetadata_test.go @@ -0,0 +1,64 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/imagekit-developer/imagekit-go" + "github.com/imagekit-developer/imagekit-go/internal/testutil" + "github.com/imagekit-developer/imagekit-go/option" +) + +func TestFileMetadataGet(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Files.Metadata.Get(context.TODO(), "fileId") + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestFileMetadataGetFromURL(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Files.Metadata.GetFromURL(context.TODO(), imagekit.FileMetadataGetFromURLParams{ + URL: "https://example.com", + }) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/fileversion.go b/fileversion.go new file mode 100644 index 0000000..1e52069 --- /dev/null +++ b/fileversion.go @@ -0,0 +1,126 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/imagekit-developer/imagekit-go/internal/apijson" + "github.com/imagekit-developer/imagekit-go/internal/requestconfig" + "github.com/imagekit-developer/imagekit-go/option" + "github.com/imagekit-developer/imagekit-go/packages/respjson" +) + +// FileVersionService contains methods and other services that help with +// interacting with the ImageKit API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewFileVersionService] method instead. +type FileVersionService struct { + Options []option.RequestOption +} + +// NewFileVersionService generates a new service that applies the given options to +// each request. These options are applied after the parent client's options (if +// there is one), and before any request-specific options. +func NewFileVersionService(opts ...option.RequestOption) (r FileVersionService) { + r = FileVersionService{} + r.Options = opts + return +} + +// This API returns details of all versions of a file. +func (r *FileVersionService) List(ctx context.Context, fileID string, opts ...option.RequestOption) (res *[]File, err error) { + opts = append(r.Options[:], opts...) + if fileID == "" { + err = errors.New("missing required fileId parameter") + return + } + path := fmt.Sprintf("v1/files/%s/versions", fileID) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return +} + +// This API deletes a non-current file version permanently. The API returns an +// empty response. +// +// Note: If you want to delete all versions of a file, use the delete file API. +func (r *FileVersionService) Delete(ctx context.Context, versionID string, body FileVersionDeleteParams, opts ...option.RequestOption) (res *FileVersionDeleteResponse, err error) { + opts = append(r.Options[:], opts...) + if body.FileID == "" { + err = errors.New("missing required fileId parameter") + return + } + if versionID == "" { + err = errors.New("missing required versionId parameter") + return + } + path := fmt.Sprintf("v1/files/%s/versions/%s", body.FileID, versionID) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, &res, opts...) + return +} + +// This API returns an object with details or attributes of a file version. +func (r *FileVersionService) Get(ctx context.Context, versionID string, query FileVersionGetParams, opts ...option.RequestOption) (res *File, err error) { + opts = append(r.Options[:], opts...) + if query.FileID == "" { + err = errors.New("missing required fileId parameter") + return + } + if versionID == "" { + err = errors.New("missing required versionId parameter") + return + } + path := fmt.Sprintf("v1/files/%s/versions/%s", query.FileID, versionID) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return +} + +// This API restores a file version as the current file version. +func (r *FileVersionService) Restore(ctx context.Context, versionID string, body FileVersionRestoreParams, opts ...option.RequestOption) (res *File, err error) { + opts = append(r.Options[:], opts...) + if body.FileID == "" { + err = errors.New("missing required fileId parameter") + return + } + if versionID == "" { + err = errors.New("missing required versionId parameter") + return + } + path := fmt.Sprintf("v1/files/%s/versions/%s/restore", body.FileID, versionID) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPut, path, nil, &res, opts...) + return +} + +type FileVersionDeleteResponse struct { + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r FileVersionDeleteResponse) RawJSON() string { return r.JSON.raw } +func (r *FileVersionDeleteResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type FileVersionDeleteParams struct { + FileID string `path:"fileId,required" json:"-"` + paramObj +} + +type FileVersionGetParams struct { + FileID string `path:"fileId,required" json:"-"` + paramObj +} + +type FileVersionRestoreParams struct { + FileID string `path:"fileId,required" json:"-"` + paramObj +} diff --git a/fileversion_test.go b/fileversion_test.go new file mode 100644 index 0000000..7299fc4 --- /dev/null +++ b/fileversion_test.go @@ -0,0 +1,128 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/imagekit-developer/imagekit-go" + "github.com/imagekit-developer/imagekit-go/internal/testutil" + "github.com/imagekit-developer/imagekit-go/option" +) + +func TestFileVersionList(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Files.Versions.List(context.TODO(), "fileId") + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestFileVersionDelete(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Files.Versions.Delete( + context.TODO(), + "versionId", + imagekit.FileVersionDeleteParams{ + FileID: "fileId", + }, + ) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestFileVersionGet(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Files.Versions.Get( + context.TODO(), + "versionId", + imagekit.FileVersionGetParams{ + FileID: "fileId", + }, + ) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestFileVersionRestore(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Files.Versions.Restore( + context.TODO(), + "versionId", + imagekit.FileVersionRestoreParams{ + FileID: "fileId", + }, + ) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/folder.go b/folder.go new file mode 100644 index 0000000..c499949 --- /dev/null +++ b/folder.go @@ -0,0 +1,285 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit + +import ( + "context" + "net/http" + + "github.com/imagekit-developer/imagekit-go/internal/apijson" + "github.com/imagekit-developer/imagekit-go/internal/requestconfig" + "github.com/imagekit-developer/imagekit-go/option" + "github.com/imagekit-developer/imagekit-go/packages/param" + "github.com/imagekit-developer/imagekit-go/packages/respjson" +) + +// FolderService contains methods and other services that help with interacting +// with the ImageKit API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewFolderService] method instead. +type FolderService struct { + Options []option.RequestOption + Job FolderJobService +} + +// NewFolderService generates a new service that applies the given options to each +// request. These options are applied after the parent client's options (if there +// is one), and before any request-specific options. +func NewFolderService(opts ...option.RequestOption) (r FolderService) { + r = FolderService{} + r.Options = opts + r.Job = NewFolderJobService(opts...) + return +} + +// This will create a new folder. You can specify the folder name and location of +// the parent folder where this new folder should be created. +func (r *FolderService) New(ctx context.Context, body FolderNewParams, opts ...option.RequestOption) (res *FolderNewResponse, err error) { + opts = append(r.Options[:], opts...) + path := "v1/folder" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +// This will delete a folder and all its contents permanently. The API returns an +// empty response. +func (r *FolderService) Delete(ctx context.Context, body FolderDeleteParams, opts ...option.RequestOption) (res *FolderDeleteResponse, err error) { + opts = append(r.Options[:], opts...) + path := "v1/folder" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, body, &res, opts...) + return +} + +// This will copy one folder into another. The selected folder, its nested folders, +// files, and their versions (in `includeVersions` is set to true) are copied in +// this operation. Note: If any file at the destination has the same name as the +// source file, then the source file and its versions will be appended to the +// destination file version history. +func (r *FolderService) Copy(ctx context.Context, body FolderCopyParams, opts ...option.RequestOption) (res *FolderCopyResponse, err error) { + opts = append(r.Options[:], opts...) + path := "v1/bulkJobs/copyFolder" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +// This will move one folder into another. The selected folder, its nested folders, +// files, and their versions are moved in this operation. Note: If any file at the +// destination has the same name as the source file, then the source file and its +// versions will be appended to the destination file version history. +func (r *FolderService) Move(ctx context.Context, body FolderMoveParams, opts ...option.RequestOption) (res *FolderMoveResponse, err error) { + opts = append(r.Options[:], opts...) + path := "v1/bulkJobs/moveFolder" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +// This API allows you to rename an existing folder. The folder and all its nested +// assets and sub-folders will remain unchanged, but their paths will be updated to +// reflect the new folder name. +func (r *FolderService) Rename(ctx context.Context, body FolderRenameParams, opts ...option.RequestOption) (res *FolderRenameResponse, err error) { + opts = append(r.Options[:], opts...) + path := "v1/bulkJobs/renameFolder" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +type FolderNewResponse struct { + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r FolderNewResponse) RawJSON() string { return r.JSON.raw } +func (r *FolderNewResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type FolderDeleteResponse struct { + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r FolderDeleteResponse) RawJSON() string { return r.JSON.raw } +func (r *FolderDeleteResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Job submitted successfully. A `jobId` will be returned. +type FolderCopyResponse struct { + // Unique identifier of the bulk job. This can be used to check the status of the + // bulk job. + JobID string `json:"jobId,required"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + JobID respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r FolderCopyResponse) RawJSON() string { return r.JSON.raw } +func (r *FolderCopyResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Job submitted successfully. A `jobId` will be returned. +type FolderMoveResponse struct { + // Unique identifier of the bulk job. This can be used to check the status of the + // bulk job. + JobID string `json:"jobId,required"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + JobID respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r FolderMoveResponse) RawJSON() string { return r.JSON.raw } +func (r *FolderMoveResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Job submitted successfully. A `jobId` will be returned. +type FolderRenameResponse struct { + // Unique identifier of the bulk job. This can be used to check the status of the + // bulk job. + JobID string `json:"jobId,required"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + JobID respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r FolderRenameResponse) RawJSON() string { return r.JSON.raw } +func (r *FolderRenameResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type FolderNewParams struct { + // The folder will be created with this name. + // + // All characters except alphabets and numbers (inclusive of unicode letters, + // marks, and numerals in other languages) will be replaced by an underscore i.e. + // `_`. + FolderName string `json:"folderName,required"` + // The folder where the new folder should be created, for root use `/` else the + // path e.g. `containing/folder/`. + // + // Note: If any folder(s) is not present in the parentFolderPath parameter, it will + // be automatically created. For example, if you pass `/product/images/summer`, + // then `product`, `images`, and `summer` folders will be created if they don't + // already exist. + ParentFolderPath string `json:"parentFolderPath,required"` + paramObj +} + +func (r FolderNewParams) MarshalJSON() (data []byte, err error) { + type shadow FolderNewParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *FolderNewParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type FolderDeleteParams struct { + // Full path to the folder you want to delete. For example `/folder/to/delete/`. + FolderPath string `json:"folderPath,required"` + paramObj +} + +func (r FolderDeleteParams) MarshalJSON() (data []byte, err error) { + type shadow FolderDeleteParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *FolderDeleteParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type FolderCopyParams struct { + // Full path to the destination folder where you want to copy the source folder + // into. + DestinationPath string `json:"destinationPath,required"` + // The full path to the source folder you want to copy. + SourceFolderPath string `json:"sourceFolderPath,required"` + // Option to copy all versions of files that are nested inside the selected folder. + // By default, only the current version of each file will be copied. When set to + // true, all versions of each file will be copied. Default value - `false`. + IncludeVersions param.Opt[bool] `json:"includeVersions,omitzero"` + paramObj +} + +func (r FolderCopyParams) MarshalJSON() (data []byte, err error) { + type shadow FolderCopyParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *FolderCopyParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type FolderMoveParams struct { + // Full path to the destination folder where you want to move the source folder + // into. + DestinationPath string `json:"destinationPath,required"` + // The full path to the source folder you want to move. + SourceFolderPath string `json:"sourceFolderPath,required"` + paramObj +} + +func (r FolderMoveParams) MarshalJSON() (data []byte, err error) { + type shadow FolderMoveParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *FolderMoveParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type FolderRenameParams struct { + // The full path to the folder you want to rename. + FolderPath string `json:"folderPath,required"` + // The new name for the folder. + // + // All characters except alphabets and numbers (inclusive of unicode letters, + // marks, and numerals in other languages) and `-` will be replaced by an + // underscore i.e. `_`. + NewFolderName string `json:"newFolderName,required"` + // Option to purge cache for the old nested files and their versions' URLs. + // + // When set to true, it will internally issue a purge cache request on CDN to + // remove the cached content of the old nested files and their versions. There will + // only be one purge request for all the nested files, which will be counted + // against your monthly purge quota. + // + // Note: A purge cache request will be issued against + // `https://ik.imagekit.io/old/folder/path*` (with a wildcard at the end). This + // will remove all nested files, their versions' URLs, and any transformations made + // using query parameters on these files or their versions. However, the cache for + // file transformations made using path parameters will persist. You can purge them + // using the purge API. For more details, refer to the purge API documentation. + // + // Default value - `false` + PurgeCache param.Opt[bool] `json:"purgeCache,omitzero"` + paramObj +} + +func (r FolderRenameParams) MarshalJSON() (data []byte, err error) { + type shadow FolderRenameParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *FolderRenameParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} diff --git a/folder_test.go b/folder_test.go new file mode 100644 index 0000000..5718b2d --- /dev/null +++ b/folder_test.go @@ -0,0 +1,150 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/imagekit-developer/imagekit-go" + "github.com/imagekit-developer/imagekit-go/internal/testutil" + "github.com/imagekit-developer/imagekit-go/option" +) + +func TestFolderNew(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Folders.New(context.TODO(), imagekit.FolderNewParams{ + FolderName: "summer", + ParentFolderPath: "/product/images/", + }) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestFolderDelete(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Folders.Delete(context.TODO(), imagekit.FolderDeleteParams{ + FolderPath: "/folder/to/delete/", + }) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestFolderCopyWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Folders.Copy(context.TODO(), imagekit.FolderCopyParams{ + DestinationPath: "/path/of/destination/folder", + SourceFolderPath: "/path/of/source/folder", + IncludeVersions: imagekit.Bool(true), + }) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestFolderMove(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Folders.Move(context.TODO(), imagekit.FolderMoveParams{ + DestinationPath: "/path/of/destination/folder", + SourceFolderPath: "/path/of/source/folder", + }) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestFolderRenameWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Folders.Rename(context.TODO(), imagekit.FolderRenameParams{ + FolderPath: "/path/of/folder", + NewFolderName: "new-folder-name", + PurgeCache: imagekit.Bool(true), + }) + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/folderjob.go b/folderjob.go new file mode 100644 index 0000000..47cf179 --- /dev/null +++ b/folderjob.go @@ -0,0 +1,94 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/imagekit-developer/imagekit-go/internal/apijson" + "github.com/imagekit-developer/imagekit-go/internal/requestconfig" + "github.com/imagekit-developer/imagekit-go/option" + "github.com/imagekit-developer/imagekit-go/packages/respjson" +) + +// FolderJobService contains methods and other services that help with interacting +// with the ImageKit API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewFolderJobService] method instead. +type FolderJobService struct { + Options []option.RequestOption +} + +// NewFolderJobService generates a new service that applies the given options to +// each request. These options are applied after the parent client's options (if +// there is one), and before any request-specific options. +func NewFolderJobService(opts ...option.RequestOption) (r FolderJobService) { + r = FolderJobService{} + r.Options = opts + return +} + +// This API returns the status of a bulk job like copy and move folder operations. +func (r *FolderJobService) Get(ctx context.Context, jobID string, opts ...option.RequestOption) (res *FolderJobGetResponse, err error) { + opts = append(r.Options[:], opts...) + if jobID == "" { + err = errors.New("missing required jobId parameter") + return + } + path := fmt.Sprintf("v1/bulkJobs/%s", jobID) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return +} + +type FolderJobGetResponse struct { + // Unique identifier of the bulk job. + JobID string `json:"jobId"` + // Unique identifier of the purge request. This will be present only if + // `purgeCache` is set to `true` in the rename folder API request. + PurgeRequestID string `json:"purgeRequestId"` + // Status of the bulk job. + // + // Any of "Pending", "Completed". + Status FolderJobGetResponseStatus `json:"status"` + // Type of the bulk job. + // + // Any of "COPY_FOLDER", "MOVE_FOLDER", "RENAME_FOLDER". + Type FolderJobGetResponseType `json:"type"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + JobID respjson.Field + PurgeRequestID respjson.Field + Status respjson.Field + Type respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r FolderJobGetResponse) RawJSON() string { return r.JSON.raw } +func (r *FolderJobGetResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Status of the bulk job. +type FolderJobGetResponseStatus string + +const ( + FolderJobGetResponseStatusPending FolderJobGetResponseStatus = "Pending" + FolderJobGetResponseStatusCompleted FolderJobGetResponseStatus = "Completed" +) + +// Type of the bulk job. +type FolderJobGetResponseType string + +const ( + FolderJobGetResponseTypeCopyFolder FolderJobGetResponseType = "COPY_FOLDER" + FolderJobGetResponseTypeMoveFolder FolderJobGetResponseType = "MOVE_FOLDER" + FolderJobGetResponseTypeRenameFolder FolderJobGetResponseType = "RENAME_FOLDER" +) diff --git a/folderjob_test.go b/folderjob_test.go new file mode 100644 index 0000000..b6ef69f --- /dev/null +++ b/folderjob_test.go @@ -0,0 +1,38 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package imagekit_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/imagekit-developer/imagekit-go" + "github.com/imagekit-developer/imagekit-go/internal/testutil" + "github.com/imagekit-developer/imagekit-go/option" +) + +func TestFolderJobGet(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := imagekit.NewClient( + option.WithBaseURL(baseURL), + option.WithPrivateAPIKey("My Private API Key"), + option.WithPassword("My Password"), + ) + _, err := client.Folders.Job.Get(context.TODO(), "jobId") + if err != nil { + var apierr *imagekit.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/go.mod b/go.mod index 9145dc0..0fea202 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,16 @@ module github.com/imagekit-developer/imagekit-go -go 1.18 +go 1.21.0 + +toolchain go1.21.13 require ( - github.com/creasty/defaults v1.6.0 - github.com/google/go-cmp v0.5.8 - github.com/google/uuid v1.3.0 - github.com/stretchr/testify v1.7.1 - gopkg.in/validator.v2 v2.0.1 + github.com/standard-webhooks/standard-webhooks/libraries v0.0.0-20250711233419-a173a6c0125c + github.com/tidwall/gjson v1.14.4 + github.com/tidwall/sjson v1.2.5 ) require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/kr/pretty v0.3.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.8.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect ) diff --git a/go.sum b/go.sum index e6d4429..318dc77 100644 --- a/go.sum +++ b/go.sum @@ -1,35 +1,12 @@ -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creasty/defaults v1.6.0 h1:ltuE9cfphUtlrBeomuu8PEyISTXnxqkBIoQfXgv7BSc= -github.com/creasty/defaults v1.6.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/validator.v2 v2.0.1 h1:xF0KWyGWXm/LM2G1TrEjqOu4pa6coO9AlWSf3msVfDY= -gopkg.in/validator.v2 v2.0.1/go.mod h1:lIUZBlB3Im4s/eYp39Ry/wkR02yOPhZ9IwIRBjuPuG8= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +github.com/standard-webhooks/standard-webhooks/libraries v0.0.0-20250711233419-a173a6c0125c h1:Mm99t6GdFMtZOwyyvu3q8gXeZX0sqnjvimTC9QCJwQc= +github.com/standard-webhooks/standard-webhooks/libraries v0.0.0-20250711233419-a173a6c0125c/go.mod h1:L1MQhA6x4dn9r007T033lsaZMv9EmBAdXyU/+EF40fo= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= diff --git a/imagekit.go b/imagekit.go deleted file mode 100644 index 100e5f6..0000000 --- a/imagekit.go +++ /dev/null @@ -1,122 +0,0 @@ -package imagekit - -import ( - "crypto/hmac" - "crypto/sha1" - "encoding/hex" - "log" - "net/http" - "strconv" - "time" - - "github.com/google/uuid" - "github.com/imagekit-developer/imagekit-go/api/media" - "github.com/imagekit-developer/imagekit-go/api/metadata" - "github.com/imagekit-developer/imagekit-go/api/uploader" - "github.com/imagekit-developer/imagekit-go/config" - "github.com/imagekit-developer/imagekit-go/logger" -) - -// ImageKit main struct -type ImageKit struct { - Config config.Configuration - Logger *logger.Logger - Media *media.API - Metadata *metadata.API - Uploader *uploader.API - getToken func() string -} - -// NewParams is a struct to define parameters to imagekit -type NewParams struct { - PrivateKey string - PublicKey string - UrlEndpoint string -} - -// New returns ImageKit object from environment variables -func New() (*ImageKit, error) { - cfg, err := config.New() - - if err != nil { - return nil, err - } - return NewFromConfiguration(cfg), nil -} - -const DefaultTokenExpire = 60 * 30 - -func getToken() string { - uuid := uuid.New() - return uuid.String() -} - -// NewFromParams returns a new ImageKit object from provided parameters -func NewFromParams(params NewParams) *ImageKit { - return NewFromConfiguration( - config.NewFromParams(params.PrivateKey, params.PublicKey, params.UrlEndpoint), - ) -} - -// NewFromConfiguration returns new ImageKit object from configuration object -func NewFromConfiguration(cfg *config.Configuration) *ImageKit { - log := logger.New() - client := &http.Client{} - - return &ImageKit{ - Config: *cfg, - Logger: log, - Media: &media.API{ - Config: *cfg, - Logger: log, - Client: client, - }, - Metadata: &metadata.API{ - Config: *cfg, - Logger: log, - Client: client, - }, - Uploader: &uploader.API{ - Config: *cfg, - Logger: log, - Client: client, - }, - getToken: getToken, - } -} - -type SignTokenParam struct { - Token string - Expires int64 - unix func() int64 -} - -type SignedToken struct { - Token string - Expires int64 - Signature string -} - -// SignToken signs given token and expiration timestamp with private key -func (ik *ImageKit) SignToken(param SignTokenParam) SignedToken { - if param.Token == "" { - param.Token = ik.getToken() - } - - if param.Expires == 0 { - var e int64 - - if param.unix == nil { - e = time.Now().Unix() - } else { - e = param.unix() - } - param.Expires = e + DefaultTokenExpire - } - - log.Println(param) - mac := hmac.New(sha1.New, []byte(ik.Config.Cloud.PrivateKey)) - mac.Write([]byte(param.Token + strconv.FormatInt(param.Expires, 10))) - signature := hex.EncodeToString(mac.Sum(nil)) - return SignedToken{Token: param.Token, Expires: param.Expires, Signature: signature} -} diff --git a/imagekit_test.go b/imagekit_test.go deleted file mode 100644 index 6e14886..0000000 --- a/imagekit_test.go +++ /dev/null @@ -1,375 +0,0 @@ -package imagekit - -import ( - neturl "net/url" - "os" - "reflect" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/imagekit-developer/imagekit-go/logger" - ikurl "github.com/imagekit-developer/imagekit-go/url" -) - -var imgkit = NewFromParams(NewParams{ - PrivateKey: "private_", - PublicKey: "public_", - UrlEndpoint: "https://ik.imagekit.io/test/", -}) - -func init() { - imgkit.Logger.SetLevel(logger.DEBUG) - os.Setenv("IMAGEKIT_PRIVATE_KEY", "private_") - os.Setenv("IMAGEKIT_PUBLIC_KEY", "public_") - os.Setenv("IMAGEKIT_ENDPOINT_URL", "https://ik.imagekit.io/test/") -} - -func Test_New(t *testing.T) { - var ik any - ik, err := New() - if err != nil { - t.Fatal(err) - } - - if ik == nil { - t.Error("null") - } - - if _, ok := ik.(*ImageKit); !ok { - t.Error("New() did not return *ImageKit") - } -} - -func TestUrl(t *testing.T) { - type tcase struct { - name string - params ikurl.UrlParam - url string - } - - cases := []tcase{ - { - name: "path", - params: ikurl.UrlParam{ - Path: "default-image.jpg", - UrlEndpoint: "https://imagekit.io/343534/", - Transformations: []map[string]any{ - { - "width": 100, - "rotation": 90, - }, - }, - }, - url: "https://imagekit.io/343534/tr:rt-90,w-100/default-image.jpg", - }, { - name: "signed-url", - params: ikurl.UrlParam{ - Path: "default-image.jpg", - Signed: true, - ExpireSeconds: 100, - UnixTime: func() int64 { - return 1653775828 - }, - }, - url: "https://ik.imagekit.io/test/default-image.jpg?ik-t=1653775928&ik-s=48842eca663c6895331331db6c90f262c601f4e8", - }, { - name: "signed-url-with-transformation", - params: ikurl.UrlParam{ - Path: "default-image.jpg", - Signed: true, - ExpireSeconds: 100, - UnixTime: func() int64 { - return 1653775828 - }, - Transformations: []map[string]any{ - { - "height": 300, - "width": 300, - }, - }, - }, - url: "https://ik.imagekit.io/test/tr:h-300,w-300/default-image.jpg?ik-t=1653775928&ik-s=1a74eab9fca6fa0bb2298aa07f4e3892a925a508", - }, - { - name: "signed-url-with-transformation-in-query", - params: ikurl.UrlParam{ - Path: "default-image.jpg", - Signed: true, - ExpireSeconds: 100, - UnixTime: func() int64 { - return 1653775828 - }, - TransformationPosition: "query", - Transformations: []map[string]any{ - { - "height": 300, - "width": 300, - }, - }, - }, - url: "https://ik.imagekit.io/test/default-image.jpg?tr=h-300%2Cw-300&ik-t=1653775928&ik-s=55f319d3a7db76e652545599a57af3dd94e32e24", - }, - { - name: "signed-url-without-ExpireSeconds", - params: ikurl.UrlParam{ - Path: "default-image.jpg", - Signed: true, - Transformations: []map[string]any{ - { - "height": 300, - "width": 300, - }, - }, - }, - url: "https://ik.imagekit.io/test/tr:h-300,w-300/default-image.jpg?ik-s=355f6c8a91031847828169116fd1d1db6e2aa8c7", - }, - { - name: "src-with-transformation", - params: ikurl.UrlParam{ - Src: "https://imagekit.io/343534/default-image.jpg", - Transformations: []map[string]any{ - { - "width": 100, - "rotation": 90, - }, - }, - }, - url: "https://imagekit.io/343534/default-image.jpg?tr=rt-90%2Cw-100", - }, { - name: "src-without-transformation", - params: ikurl.UrlParam{ - Src: "https://imagekit.io/343534/default-image.jpg", - }, - url: "https://imagekit.io/343534/default-image.jpg", - }, { - name: "trquery", - params: ikurl.UrlParam{ - Path: "default-image.jpg", - UrlEndpoint: "https://imagekit.io/343534/", - Transformations: []map[string]any{ - { - "width": 100, - "rotation": 90, - }, - }, - TransformationPosition: ikurl.QUERY, - }, - url: "https://imagekit.io/343534/default-image.jpg?tr=rt-90%2Cw-100", - }, { - name: "transformations", - params: ikurl.UrlParam{ - Path: "default-image.jpg", - UrlEndpoint: "https://ik.imagekit.io/dk1m7xkgi/", - Transformations: []map[string]any{ - { - "width": 200, - "height": 400, - "cropMode": "extract", - "focus": "center", - "x": 100, - "y": 110, - "quality": 85, - "format": "auto", - "blur": 5, - "dpr": "auto", - "effectGray": "-", - "defaultImage": "/test2_hBIIEweBy.gif", - "progressive": true, - "lossless": true, - "trim": true, - "border": "5_005500", - "colorProfile": true, - "metadata": true, - "rotation": "auto", - "radius": 40, - "background": "344222", - "effectContrast": "-", - "effectSharpen": "-", - "raw": "x-1", - }, - }, - }, - url: "https://ik.imagekit.io/dk1m7xkgi/tr:b-5_005500,bg-344222,bl-5,cm-extract,cp-true,di-test2_hBIIEweBy.gif,dpr-auto,e-contrast,e-grayscale,e-sharpen,f-auto,fo-center,h-400,lo-true,md-true,pr-true,q-85,r-40,rt-auto,t-true,w-200,x-1,x-100,y-110/default-image.jpg", - }, { - name: "aspect-ratio-xc-yc", - params: ikurl.UrlParam{ - Path: "default-image.jpg", - Transformations: []map[string]any{ - { - "width": 200, - "aspectRatio": "16-9", - "cropMode": "extract", - "focus": "center", - "xc": 100, - "yc": 110, - "quality": 85, - "format": "auto", - "blur": 50, - "dpr": 2, - "rotation": 90, - "effectSharpen": 40, - }, - }, - }, - url: "https://ik.imagekit.io/test/tr:ar-16-9,bl-50,cm-extract,dpr-2,e-sharpen-40,f-auto,fo-center,q-85,rt-90,w-200,xc-100,yc-110/default-image.jpg", - }, { - name: "unsharp-mask", - params: ikurl.UrlParam{ - Path: "default-image.jpg", - Transformations: []map[string]any{ - { - "effectUSM": "2-2-0.8-0.024", - }, - }, - }, - url: "https://ik.imagekit.io/test/tr:e-usm-2-2-0.8-0.024/default-image.jpg", - }, { - name: "chained transformations", - params: ikurl.UrlParam{ - Path: "default-image.jpg", - UrlEndpoint: "https://ik.imagekit.io/343534/", - Transformations: []map[string]any{ - { - "width": 100, - "height": 200, - }, { - "rotation": 90, - }, - }, - }, - url: "https://ik.imagekit.io/343534/tr:h-200,w-100:rt-90/default-image.jpg", - }, { - - name: "common-overlay-options", - params: ikurl.UrlParam{ - Path: "default-image.jpg", - Transformations: []map[string]any{ - { - "overlayX": 100, - "overlayY": 110, - "overlayHeight": 100, - "overlayWidth": 90, - "overlayBackground": "443322", - "overlayFocus": "bottom", - }, - }, - }, - url: "https://ik.imagekit.io/test/tr:obg-443322,ofo-bottom,oh-100,ow-90,ox-100,oy-110/default-image.jpg", - }, { - name: "text-overlay-options", - params: ikurl.UrlParam{ - Path: "default-image.jpg", - Transformations: []map[string]any{ - { - "overlayText": "this is a sample overlay", - "overlayX": 100, - "overlayY": 110, - "overlayHeight": 500, - "overlayWidth": 900, - "overlayTextPadding": "20_40", - "overlayTextBackground": "ffffff", - "overlayTextInnerAlignment": "right", - "overlayTextColor": "blue", - "overlayTextFontFamily": "Arvo", - "overlayTextFontSize": 40, - "overlayTextTypography": "ib", - "overlayRadius": 20, - }, - }, - }, - url: "https://ik.imagekit.io/test/tr:oh-500,or-20,ot-this%20is%20a%20sample%20overlay,otbg-ffffff,otc-blue,otf-Arvo,otia-right,otp-20_40,ots-40,ott-ib,ow-900,ox-100,oy-110/default-image.jpg", - }, { - name: "image-overlay-options", - params: ikurl.UrlParam{ - Path: "default-image.jpg", - Transformations: []map[string]any{ - { - "overlayImage": "/test2_hBIIEweBy.gif", - "overlayX": 100, - "overlayY": 110, - "overlayHeight": 200, - "overlayWidth": 200, - "overlayImageBorder": "4_blue", - "overlayImageDPR": 0.2, - "overlayImageQuality": 80, - "overlayImageCropping": "at_max", - "overlayImageX": 100, - "overlayImageY": 20, - "overlayImageTrim": false, - }, - }, - }, - url: "https://ik.imagekit.io/test/tr:oh-200,oi-test2_hBIIEweBy.gif,oib-4_blue,oic-at_max,oidpr-0.2,oiq-80,oit-false,oix-100,oiy-20,ow-200,ox-100,oy-110/default-image.jpg", - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - url, err := imgkit.Url(tc.params) - - if err != nil { - t.Errorf(err.Error()) - } - if !urlsEquals(url, tc.url) { - t.Errorf("expected url: %s\ngot: %s", tc.url, url) - } - }) - } - -} - -func urlsEquals(url1 string, url2 string) bool { - u1, _ := neturl.Parse(url1) - u2, _ := neturl.Parse(url2) - - q1 := u1.Query() - q2 := u2.Query() - - u1.RawQuery = "" - u2.RawQuery = "" - - if u1.String() != u2.String() { - return false - } - - return reflect.DeepEqual(q1, q2) -} - -func Test_SignToken(t *testing.T) { - var expire int64 = 1655379249 + DefaultTokenExpire - var unix = func() int64 { return 1655379249 } - var token = "xxxx-xxxx-xxxxxxxx" - - cases := map[string]struct { - param SignTokenParam - result SignedToken - }{ - "with param": { - param: SignTokenParam{Token: "31c468de-520a-4dc1-8868-de1e0fb93a7b", Expires: 1655379249}, - result: SignedToken{Token: "31c468de-520a-4dc1-8868-de1e0fb93a7b", Expires: 1655379249, Signature: "ed6f1aadeec33eb3509c0576e6a05100861c64c5"}, - }, - "use defaults": { - param: SignTokenParam{Token: "31c468de-520a-4dc1-8868-de1e0fb93a7b", unix: unix}, - result: SignedToken{Token: "31c468de-520a-4dc1-8868-de1e0fb93a7b", Expires: expire, Signature: "b3c708386f56dbcdac2a6e650ab00789ec37645e"}, - }, - "autogen-token": { - param: SignTokenParam{unix: unix}, - result: SignedToken{Token: token, Expires: expire, Signature: "c46ef585f970b560aea69b90e32cd002c6639515"}, - }, - } - - imgkit.getToken = func() string { - return token - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - resp := imgkit.SignToken(tc.param) - - if !cmp.Equal(resp, tc.result) { - t.Errorf("%v\n%v", resp, tc.result) - } - }) - } - -} diff --git a/internal/apierror/apierror.go b/internal/apierror/apierror.go new file mode 100644 index 0000000..b3a414b --- /dev/null +++ b/internal/apierror/apierror.go @@ -0,0 +1,50 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package apierror + +import ( + "fmt" + "net/http" + "net/http/httputil" + + "github.com/imagekit-developer/imagekit-go/internal/apijson" + "github.com/imagekit-developer/imagekit-go/packages/respjson" +) + +// Error represents an error that originates from the API, i.e. when a request is +// made and the API returns a response with a HTTP status code. Other errors are +// not wrapped by this SDK. +type Error struct { + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` + StatusCode int + Request *http.Request + Response *http.Response +} + +// Returns the unmodified JSON received from the API +func (r Error) RawJSON() string { return r.JSON.raw } +func (r *Error) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +func (r *Error) Error() string { + // Attempt to re-populate the response body + return fmt.Sprintf("%s %q: %d %s %s", r.Request.Method, r.Request.URL, r.Response.StatusCode, http.StatusText(r.Response.StatusCode), r.JSON.raw) +} + +func (r *Error) DumpRequest(body bool) []byte { + if r.Request.GetBody != nil { + r.Request.Body, _ = r.Request.GetBody() + } + out, _ := httputil.DumpRequestOut(r.Request, body) + return out +} + +func (r *Error) DumpResponse(body bool) []byte { + out, _ := httputil.DumpResponse(r.Response, body) + return out +} diff --git a/internal/apiform/encoder.go b/internal/apiform/encoder.go new file mode 100644 index 0000000..1028a4a --- /dev/null +++ b/internal/apiform/encoder.go @@ -0,0 +1,465 @@ +package apiform + +import ( + "fmt" + "io" + "mime/multipart" + "net/textproto" + "path" + "reflect" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/imagekit-developer/imagekit-go/packages/param" +) + +var encoders sync.Map // map[encoderEntry]encoderFunc + +func Marshal(value any, writer *multipart.Writer) error { + e := &encoder{ + dateFormat: time.RFC3339, + arrayFmt: "comma", + } + return e.marshal(value, writer) +} + +func MarshalRoot(value any, writer *multipart.Writer) error { + e := &encoder{ + root: true, + dateFormat: time.RFC3339, + arrayFmt: "comma", + } + return e.marshal(value, writer) +} + +func MarshalWithSettings(value any, writer *multipart.Writer, arrayFormat string) error { + e := &encoder{ + arrayFmt: arrayFormat, + dateFormat: time.RFC3339, + } + return e.marshal(value, writer) +} + +type encoder struct { + arrayFmt string + dateFormat string + root bool +} + +type encoderFunc func(key string, value reflect.Value, writer *multipart.Writer) error + +type encoderField struct { + tag parsedStructTag + fn encoderFunc + idx []int +} + +type encoderEntry struct { + reflect.Type + dateFormat string + root bool +} + +func (e *encoder) marshal(value any, writer *multipart.Writer) error { + val := reflect.ValueOf(value) + if !val.IsValid() { + return nil + } + typ := val.Type() + enc := e.typeEncoder(typ) + return enc("", val, writer) +} + +func (e *encoder) typeEncoder(t reflect.Type) encoderFunc { + entry := encoderEntry{ + Type: t, + dateFormat: e.dateFormat, + root: e.root, + } + + if fi, ok := encoders.Load(entry); ok { + return fi.(encoderFunc) + } + + // To deal with recursive types, populate the map with an + // indirect func before we build it. This type waits on the + // real func (f) to be ready and then calls it. This indirect + // func is only used for recursive types. + var ( + wg sync.WaitGroup + f encoderFunc + ) + wg.Add(1) + fi, loaded := encoders.LoadOrStore(entry, encoderFunc(func(key string, v reflect.Value, writer *multipart.Writer) error { + wg.Wait() + return f(key, v, writer) + })) + if loaded { + return fi.(encoderFunc) + } + + // Compute the real encoder and replace the indirect func with it. + f = e.newTypeEncoder(t) + wg.Done() + encoders.Store(entry, f) + return f +} + +func (e *encoder) newTypeEncoder(t reflect.Type) encoderFunc { + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + return e.newTimeTypeEncoder() + } + if t.Implements(reflect.TypeOf((*io.Reader)(nil)).Elem()) { + return e.newReaderTypeEncoder() + } + e.root = false + switch t.Kind() { + case reflect.Pointer: + inner := t.Elem() + + innerEncoder := e.typeEncoder(inner) + return func(key string, v reflect.Value, writer *multipart.Writer) error { + if !v.IsValid() || v.IsNil() { + return nil + } + return innerEncoder(key, v.Elem(), writer) + } + case reflect.Struct: + return e.newStructTypeEncoder(t) + case reflect.Slice, reflect.Array: + return e.newArrayTypeEncoder(t) + case reflect.Map: + return e.newMapEncoder(t) + case reflect.Interface: + return e.newInterfaceEncoder() + default: + return e.newPrimitiveTypeEncoder(t) + } +} + +func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc { + switch t.Kind() { + // Note that we could use `gjson` to encode these types but it would complicate our + // code more and this current code shouldn't cause any issues + case reflect.String: + return func(key string, v reflect.Value, writer *multipart.Writer) error { + return writer.WriteField(key, v.String()) + } + case reflect.Bool: + return func(key string, v reflect.Value, writer *multipart.Writer) error { + if v.Bool() { + return writer.WriteField(key, "true") + } + return writer.WriteField(key, "false") + } + case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64: + return func(key string, v reflect.Value, writer *multipart.Writer) error { + return writer.WriteField(key, strconv.FormatInt(v.Int(), 10)) + } + case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return func(key string, v reflect.Value, writer *multipart.Writer) error { + return writer.WriteField(key, strconv.FormatUint(v.Uint(), 10)) + } + case reflect.Float32: + return func(key string, v reflect.Value, writer *multipart.Writer) error { + return writer.WriteField(key, strconv.FormatFloat(v.Float(), 'f', -1, 32)) + } + case reflect.Float64: + return func(key string, v reflect.Value, writer *multipart.Writer) error { + return writer.WriteField(key, strconv.FormatFloat(v.Float(), 'f', -1, 64)) + } + default: + return func(key string, v reflect.Value, writer *multipart.Writer) error { + return fmt.Errorf("unknown type received at primitive encoder: %s", t.String()) + } + } +} + +func arrayKeyEncoder(arrayFmt string) func(string, int) string { + var keyFn func(string, int) string + switch arrayFmt { + case "comma", "repeat": + keyFn = func(k string, _ int) string { return k } + case "brackets": + keyFn = func(key string, _ int) string { return key + "[]" } + case "indices:dots": + keyFn = func(k string, i int) string { + if k == "" { + return strconv.Itoa(i) + } + return k + "." + strconv.Itoa(i) + } + case "indices:brackets": + keyFn = func(k string, i int) string { + if k == "" { + return strconv.Itoa(i) + } + return k + "[" + strconv.Itoa(i) + "]" + } + } + return keyFn +} + +func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc { + itemEncoder := e.typeEncoder(t.Elem()) + keyFn := arrayKeyEncoder(e.arrayFmt) + return func(key string, v reflect.Value, writer *multipart.Writer) error { + if keyFn == nil { + return fmt.Errorf("apiform: unsupported array format") + } + for i := 0; i < v.Len(); i++ { + err := itemEncoder(keyFn(key, i), v.Index(i), writer) + if err != nil { + return err + } + } + return nil + } +} + +func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { + if t.Implements(reflect.TypeOf((*param.Optional)(nil)).Elem()) { + return e.newRichFieldTypeEncoder(t) + } + + for i := 0; i < t.NumField(); i++ { + if t.Field(i).Type == paramUnionType && t.Field(i).Anonymous { + return e.newStructUnionTypeEncoder(t) + } + } + + encoderFields := []encoderField{} + extraEncoder := (*encoderField)(nil) + + // This helper allows us to recursively collect field encoders into a flat + // array. The parameter `index` keeps track of the access patterns necessary + // to get to some field. + var collectEncoderFields func(r reflect.Type, index []int) + collectEncoderFields = func(r reflect.Type, index []int) { + for i := 0; i < r.NumField(); i++ { + idx := append(index, i) + field := t.FieldByIndex(idx) + if !field.IsExported() { + continue + } + // If this is an embedded struct, traverse one level deeper to extract + // the field and get their encoders as well. + if field.Anonymous { + collectEncoderFields(field.Type, idx) + continue + } + // If json tag is not present, then we skip, which is intentionally + // different behavior from the stdlib. + ptag, ok := parseFormStructTag(field) + if !ok { + continue + } + // We only want to support unexported field if they're tagged with + // `extras` because that field shouldn't be part of the public API. We + // also want to only keep the top level extras + if ptag.extras && len(index) == 0 { + extraEncoder = &encoderField{ptag, e.typeEncoder(field.Type.Elem()), idx} + continue + } + if ptag.name == "-" || ptag.name == "" { + continue + } + + dateFormat, ok := parseFormatStructTag(field) + oldFormat := e.dateFormat + if ok { + switch dateFormat { + case "date-time": + e.dateFormat = time.RFC3339 + case "date": + e.dateFormat = "2006-01-02" + } + } + + var encoderFn encoderFunc + if ptag.omitzero { + typeEncoderFn := e.typeEncoder(field.Type) + encoderFn = func(key string, value reflect.Value, writer *multipart.Writer) error { + if value.IsZero() { + return nil + } + return typeEncoderFn(key, value, writer) + } + } else { + encoderFn = e.typeEncoder(field.Type) + } + encoderFields = append(encoderFields, encoderField{ptag, encoderFn, idx}) + e.dateFormat = oldFormat + } + } + collectEncoderFields(t, []int{}) + + // Ensure deterministic output by sorting by lexicographic order + sort.Slice(encoderFields, func(i, j int) bool { + return encoderFields[i].tag.name < encoderFields[j].tag.name + }) + + return func(key string, value reflect.Value, writer *multipart.Writer) error { + if key != "" { + key = key + "." + } + + for _, ef := range encoderFields { + field := value.FieldByIndex(ef.idx) + err := ef.fn(key+ef.tag.name, field, writer) + if err != nil { + return err + } + } + + if extraEncoder != nil { + err := e.encodeMapEntries(key, value.FieldByIndex(extraEncoder.idx), writer) + if err != nil { + return err + } + } + + return nil + } +} + +var paramUnionType = reflect.TypeOf((*param.APIUnion)(nil)).Elem() + +func (e *encoder) newStructUnionTypeEncoder(t reflect.Type) encoderFunc { + var fieldEncoders []encoderFunc + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + if field.Type == paramUnionType && field.Anonymous { + fieldEncoders = append(fieldEncoders, nil) + continue + } + fieldEncoders = append(fieldEncoders, e.typeEncoder(field.Type)) + } + + return func(key string, value reflect.Value, writer *multipart.Writer) error { + for i := 0; i < t.NumField(); i++ { + if value.Field(i).Type() == paramUnionType { + continue + } + if !value.Field(i).IsZero() { + return fieldEncoders[i](key, value.Field(i), writer) + } + } + return fmt.Errorf("apiform: union %s has no field set", t.String()) + } +} + +func (e *encoder) newTimeTypeEncoder() encoderFunc { + format := e.dateFormat + return func(key string, value reflect.Value, writer *multipart.Writer) error { + return writer.WriteField(key, value.Convert(reflect.TypeOf(time.Time{})).Interface().(time.Time).Format(format)) + } +} + +func (e encoder) newInterfaceEncoder() encoderFunc { + return func(key string, value reflect.Value, writer *multipart.Writer) error { + value = value.Elem() + if !value.IsValid() { + return nil + } + return e.typeEncoder(value.Type())(key, value, writer) + } +} + +var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") + +func escapeQuotes(s string) string { + return quoteEscaper.Replace(s) +} + +func (e *encoder) newReaderTypeEncoder() encoderFunc { + return func(key string, value reflect.Value, writer *multipart.Writer) error { + reader, ok := value.Convert(reflect.TypeOf((*io.Reader)(nil)).Elem()).Interface().(io.Reader) + if !ok { + return nil + } + filename := "anonymous_file" + contentType := "application/octet-stream" + if named, ok := reader.(interface{ Filename() string }); ok { + filename = named.Filename() + } else if named, ok := reader.(interface{ Name() string }); ok { + filename = path.Base(named.Name()) + } + if typed, ok := reader.(interface{ ContentType() string }); ok { + contentType = typed.ContentType() + } + + // Below is taken almost 1-for-1 from [multipart.CreateFormFile] + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeQuotes(key), escapeQuotes(filename))) + h.Set("Content-Type", contentType) + filewriter, err := writer.CreatePart(h) + if err != nil { + return err + } + _, err = io.Copy(filewriter, reader) + return err + } +} + +// Given a []byte of json (may either be an empty object or an object that already contains entries) +// encode all of the entries in the map to the json byte array. +func (e *encoder) encodeMapEntries(key string, v reflect.Value, writer *multipart.Writer) error { + type mapPair struct { + key string + value reflect.Value + } + + if key != "" { + key = key + "." + } + + pairs := []mapPair{} + + iter := v.MapRange() + for iter.Next() { + if iter.Key().Type().Kind() == reflect.String { + pairs = append(pairs, mapPair{key: iter.Key().String(), value: iter.Value()}) + } else { + return fmt.Errorf("cannot encode a map with a non string key") + } + } + + // Ensure deterministic output + sort.Slice(pairs, func(i, j int) bool { + return pairs[i].key < pairs[j].key + }) + + elementEncoder := e.typeEncoder(v.Type().Elem()) + for _, p := range pairs { + err := elementEncoder(key+string(p.key), p.value, writer) + if err != nil { + return err + } + } + + return nil +} + +func (e *encoder) newMapEncoder(_ reflect.Type) encoderFunc { + return func(key string, value reflect.Value, writer *multipart.Writer) error { + return e.encodeMapEntries(key, value, writer) + } +} + +func WriteExtras(writer *multipart.Writer, extras map[string]any) (err error) { + for k, v := range extras { + str, ok := v.(string) + if !ok { + break + } + err = writer.WriteField(k, str) + if err != nil { + break + } + } + return +} diff --git a/internal/apiform/form.go b/internal/apiform/form.go new file mode 100644 index 0000000..5445116 --- /dev/null +++ b/internal/apiform/form.go @@ -0,0 +1,5 @@ +package apiform + +type Marshaler interface { + MarshalMultipart() ([]byte, string, error) +} diff --git a/internal/apiform/form_test.go b/internal/apiform/form_test.go new file mode 100644 index 0000000..53e2c48 --- /dev/null +++ b/internal/apiform/form_test.go @@ -0,0 +1,560 @@ +package apiform + +import ( + "bytes" + "github.com/imagekit-developer/imagekit-go/packages/param" + "io" + "mime/multipart" + "strings" + "testing" + "time" +) + +func P[T any](v T) *T { return &v } + +type Primitives struct { + A bool `form:"a"` + B int `form:"b"` + C uint `form:"c"` + D float64 `form:"d"` + E float32 `form:"e"` + F []int `form:"f"` +} + +// These aliases are necessary to bypass the cache. +// This only relevant during testing. +type int_ int +type PrimitivesBrackets struct { + F []int_ `form:"f"` +} + +type PrimitivePointers struct { + A *bool `form:"a"` + B *int `form:"b"` + C *uint `form:"c"` + D *float64 `form:"d"` + E *float32 `form:"e"` + F *[]int `form:"f"` +} + +type Slices struct { + Slice []Primitives `form:"slices"` +} + +type DateTime struct { + Date time.Time `form:"date" format:"date"` + DateTime time.Time `form:"date-time" format:"date-time"` +} + +type AdditionalProperties struct { + A bool `form:"a"` + Extras map[string]any `form:"-,extras"` +} + +type TypedAdditionalProperties struct { + A bool `form:"a"` + Extras map[string]int `form:"-,extras"` +} + +type EmbeddedStructs struct { + AdditionalProperties + A *int `form:"number2"` + Extras map[string]any `form:"-,extras"` +} + +type Recursive struct { + Name string `form:"name"` + Child *Recursive `form:"child"` +} + +type UnknownStruct struct { + Unknown any `form:"unknown"` +} + +type UnionStruct struct { + Union Union `form:"union" format:"date"` +} + +type Union interface { + union() +} + +type UnionInteger int64 + +func (UnionInteger) union() {} + +type UnionStructA struct { + Type string `form:"type"` + A string `form:"a"` + B string `form:"b"` +} + +func (UnionStructA) union() {} + +type UnionStructB struct { + Type string `form:"type"` + A string `form:"a"` +} + +func (UnionStructB) union() {} + +type UnionTime time.Time + +func (UnionTime) union() {} + +type ReaderStruct struct { + File io.Reader `form:"file"` +} + +type NamedEnum string + +const NamedEnumFoo NamedEnum = "foo" + +type StructUnionWrapper struct { + Union StructUnion `form:"union"` +} + +type StructUnion struct { + OfInt param.Opt[int64] `form:",omitzero,inline"` + OfString param.Opt[string] `form:",omitzero,inline"` + OfEnum param.Opt[NamedEnum] `form:",omitzero,inline"` + OfA UnionStructA `form:",omitzero,inline"` + OfB UnionStructB `form:",omitzero,inline"` + param.APIUnion +} + +var tests = map[string]struct { + buf string + val any +}{ + "file": { + buf: `--xxx +Content-Disposition: form-data; name="file"; filename="anonymous_file" +Content-Type: application/octet-stream + +some file contents... +--xxx-- +`, + val: ReaderStruct{ + File: io.Reader(bytes.NewBuffer([]byte("some file contents..."))), + }, + }, + "map_string": { + `--xxx +Content-Disposition: form-data; name="foo" + +bar +--xxx-- +`, + map[string]string{"foo": "bar"}, + }, + + "map_interface": { + `--xxx +Content-Disposition: form-data; name="a" + +1 +--xxx +Content-Disposition: form-data; name="b" + +str +--xxx +Content-Disposition: form-data; name="c" + +false +--xxx-- +`, + map[string]any{"a": float64(1), "b": "str", "c": false}, + }, + + "primitive_struct": { + `--xxx +Content-Disposition: form-data; name="a" + +false +--xxx +Content-Disposition: form-data; name="b" + +237628372683 +--xxx +Content-Disposition: form-data; name="c" + +654 +--xxx +Content-Disposition: form-data; name="d" + +9999.43 +--xxx +Content-Disposition: form-data; name="e" + +43.76 +--xxx +Content-Disposition: form-data; name="f.0" + +1 +--xxx +Content-Disposition: form-data; name="f.1" + +2 +--xxx +Content-Disposition: form-data; name="f.2" + +3 +--xxx +Content-Disposition: form-data; name="f.3" + +4 +--xxx-- +`, + Primitives{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}, + }, + "primitive_struct,brackets": { + `--xxx +Content-Disposition: form-data; name="f[]" + +1 +--xxx +Content-Disposition: form-data; name="f[]" + +2 +--xxx +Content-Disposition: form-data; name="f[]" + +3 +--xxx +Content-Disposition: form-data; name="f[]" + +4 +--xxx-- +`, + PrimitivesBrackets{F: []int_{1, 2, 3, 4}}, + }, + + "slices": { + `--xxx +Content-Disposition: form-data; name="slices.0.a" + +false +--xxx +Content-Disposition: form-data; name="slices.0.b" + +237628372683 +--xxx +Content-Disposition: form-data; name="slices.0.c" + +654 +--xxx +Content-Disposition: form-data; name="slices.0.d" + +9999.43 +--xxx +Content-Disposition: form-data; name="slices.0.e" + +43.76 +--xxx +Content-Disposition: form-data; name="slices.0.f.0" + +1 +--xxx +Content-Disposition: form-data; name="slices.0.f.1" + +2 +--xxx +Content-Disposition: form-data; name="slices.0.f.2" + +3 +--xxx +Content-Disposition: form-data; name="slices.0.f.3" + +4 +--xxx-- +`, + Slices{ + Slice: []Primitives{{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}}, + }, + }, + "primitive_pointer_struct": { + `--xxx +Content-Disposition: form-data; name="a" + +false +--xxx +Content-Disposition: form-data; name="b" + +237628372683 +--xxx +Content-Disposition: form-data; name="c" + +654 +--xxx +Content-Disposition: form-data; name="d" + +9999.43 +--xxx +Content-Disposition: form-data; name="e" + +43.76 +--xxx +Content-Disposition: form-data; name="f.0" + +1 +--xxx +Content-Disposition: form-data; name="f.1" + +2 +--xxx +Content-Disposition: form-data; name="f.2" + +3 +--xxx +Content-Disposition: form-data; name="f.3" + +4 +--xxx +Content-Disposition: form-data; name="f.4" + +5 +--xxx-- +`, + PrimitivePointers{ + A: P(false), + B: P(237628372683), + C: P(uint(654)), + D: P(9999.43), + E: P(float32(43.76)), + F: &[]int{1, 2, 3, 4, 5}, + }, + }, + + "datetime_struct": { + `--xxx +Content-Disposition: form-data; name="date" + +2006-01-02 +--xxx +Content-Disposition: form-data; name="date-time" + +2006-01-02T15:04:05Z +--xxx-- +`, + DateTime{ + Date: time.Date(2006, time.January, 2, 0, 0, 0, 0, time.UTC), + DateTime: time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC), + }, + }, + + "additional_properties": { + `--xxx +Content-Disposition: form-data; name="a" + +true +--xxx +Content-Disposition: form-data; name="bar" + +value +--xxx +Content-Disposition: form-data; name="foo" + +true +--xxx-- +`, + AdditionalProperties{ + A: true, + Extras: map[string]any{ + "bar": "value", + "foo": true, + }, + }, + }, + + "recursive_struct": { + `--xxx +Content-Disposition: form-data; name="child.name" + +Alex +--xxx +Content-Disposition: form-data; name="name" + +Robert +--xxx-- +`, + Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}}, + }, + + "unknown_struct_number": { + `--xxx +Content-Disposition: form-data; name="unknown" + +12 +--xxx-- +`, + UnknownStruct{ + Unknown: 12., + }, + }, + + "unknown_struct_map": { + `--xxx +Content-Disposition: form-data; name="unknown.foo" + +bar +--xxx-- +`, + UnknownStruct{ + Unknown: map[string]any{ + "foo": "bar", + }, + }, + }, + + "struct_union_integer": { + `--xxx +Content-Disposition: form-data; name="union" + +12 +--xxx-- +`, + StructUnionWrapper{ + Union: StructUnion{OfInt: param.NewOpt[int64](12)}, + }, + }, + + "union_integer": { + `--xxx +Content-Disposition: form-data; name="union" + +12 +--xxx-- +`, + UnionStruct{ + Union: UnionInteger(12), + }, + }, + + "struct_union_struct_discriminated_a": { + `--xxx +Content-Disposition: form-data; name="union.a" + +foo +--xxx +Content-Disposition: form-data; name="union.b" + +bar +--xxx +Content-Disposition: form-data; name="union.type" + +typeA +--xxx-- +`, + StructUnionWrapper{ + Union: StructUnion{OfA: UnionStructA{ + Type: "typeA", + A: "foo", + B: "bar", + }}, + }, + }, + + "union_struct_discriminated_a": { + `--xxx +Content-Disposition: form-data; name="union.a" + +foo +--xxx +Content-Disposition: form-data; name="union.b" + +bar +--xxx +Content-Disposition: form-data; name="union.type" + +typeA +--xxx-- +`, + + UnionStruct{ + Union: UnionStructA{ + Type: "typeA", + A: "foo", + B: "bar", + }, + }, + }, + + "struct_union_struct_discriminated_b": { + `--xxx +Content-Disposition: form-data; name="union.a" + +foo +--xxx +Content-Disposition: form-data; name="union.type" + +typeB +--xxx-- +`, + StructUnionWrapper{ + Union: StructUnion{OfB: UnionStructB{ + Type: "typeB", + A: "foo", + }}, + }, + }, + + "union_struct_discriminated_b": { + `--xxx +Content-Disposition: form-data; name="union.a" + +foo +--xxx +Content-Disposition: form-data; name="union.type" + +typeB +--xxx-- +`, + UnionStruct{ + Union: UnionStructB{ + Type: "typeB", + A: "foo", + }, + }, + }, + + "union_struct_time": { + `--xxx +Content-Disposition: form-data; name="union" + +2010-05-23 +--xxx-- +`, + UnionStruct{ + Union: UnionTime(time.Date(2010, 05, 23, 0, 0, 0, 0, time.UTC)), + }, + }, +} + +func TestEncode(t *testing.T) { + for name, test := range tests { + t.Run(name, func(t *testing.T) { + buf := bytes.NewBuffer(nil) + writer := multipart.NewWriter(buf) + writer.SetBoundary("xxx") + + var arrayFmt string = "indices:dots" + if tags := strings.Split(name, ","); len(tags) > 1 { + arrayFmt = tags[1] + } + + err := MarshalWithSettings(test.val, writer, arrayFmt) + if err != nil { + t.Errorf("serialization of %v failed with error %v", test.val, err) + } + err = writer.Close() + if err != nil { + t.Errorf("serialization of %v failed with error %v", test.val, err) + } + raw := buf.Bytes() + if string(raw) != strings.ReplaceAll(test.buf, "\n", "\r\n") { + t.Errorf("expected %+#v to serialize to '%s' but got '%s'", test.val, test.buf, string(raw)) + } + }) + } +} diff --git a/internal/apiform/richparam.go b/internal/apiform/richparam.go new file mode 100644 index 0000000..48ac937 --- /dev/null +++ b/internal/apiform/richparam.go @@ -0,0 +1,20 @@ +package apiform + +import ( + "github.com/imagekit-developer/imagekit-go/packages/param" + "mime/multipart" + "reflect" +) + +func (e *encoder) newRichFieldTypeEncoder(t reflect.Type) encoderFunc { + f, _ := t.FieldByName("Value") + enc := e.newPrimitiveTypeEncoder(f.Type) + return func(key string, value reflect.Value, writer *multipart.Writer) error { + if opt, ok := value.Interface().(param.Optional); ok && opt.Valid() { + return enc(key, value.FieldByIndex(f.Index), writer) + } else if ok && param.IsNull(opt) { + return writer.WriteField(key, "null") + } + return nil + } +} diff --git a/internal/apiform/tag.go b/internal/apiform/tag.go new file mode 100644 index 0000000..736fc1e --- /dev/null +++ b/internal/apiform/tag.go @@ -0,0 +1,51 @@ +package apiform + +import ( + "reflect" + "strings" +) + +const jsonStructTag = "json" +const formStructTag = "form" +const formatStructTag = "format" + +type parsedStructTag struct { + name string + required bool + extras bool + metadata bool + omitzero bool +} + +func parseFormStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) { + raw, ok := field.Tag.Lookup(formStructTag) + if !ok { + raw, ok = field.Tag.Lookup(jsonStructTag) + } + if !ok { + return + } + parts := strings.Split(raw, ",") + if len(parts) == 0 { + return tag, false + } + tag.name = parts[0] + for _, part := range parts[1:] { + switch part { + case "required": + tag.required = true + case "extras": + tag.extras = true + case "metadata": + tag.metadata = true + case "omitzero": + tag.omitzero = true + } + } + return +} + +func parseFormatStructTag(field reflect.StructField) (format string, ok bool) { + format, ok = field.Tag.Lookup(formatStructTag) + return +} diff --git a/internal/apijson/decodeparam_test.go b/internal/apijson/decodeparam_test.go new file mode 100644 index 0000000..51feb66 --- /dev/null +++ b/internal/apijson/decodeparam_test.go @@ -0,0 +1,498 @@ +package apijson_test + +import ( + "encoding/json" + "fmt" + "github.com/imagekit-developer/imagekit-go/internal/apijson" + "github.com/imagekit-developer/imagekit-go/packages/param" + "reflect" + "testing" +) + +func TestOptionalDecoders(t *testing.T) { + cases := map[string]struct { + buf string + val any + }{ + + "opt_string_present": { + `"hello"`, + param.NewOpt("hello"), + }, + "opt_string_empty_present": { + `""`, + param.NewOpt(""), + }, + "opt_string_null": { + `null`, + param.Null[string](), + }, + "opt_string_null_with_whitespace": { + ` null `, + param.Null[string](), + }, + } + + for name, test := range cases { + t.Run(name, func(t *testing.T) { + result := reflect.New(reflect.TypeOf(test.val)) + if err := json.Unmarshal([]byte(test.buf), result.Interface()); err != nil { + t.Fatalf("deserialization of %v failed with error %v", result, err) + } + + if !reflect.DeepEqual(result.Elem().Interface(), test.val) { + t.Fatalf("expected '%s' to deserialize to \n%#v\nbut got\n%#v", test.buf, test.val, result.Elem().Interface()) + } + }) + } +} + +type paramObject = param.APIObject + +type BasicObject struct { + ReqInt int64 `json:"req_int,required"` + ReqFloat float64 `json:"req_float,required"` + ReqString string `json:"req_string,required"` + ReqBool bool `json:"req_bool,required"` + + OptInt param.Opt[int64] `json:"opt_int"` + OptFloat param.Opt[float64] `json:"opt_float"` + OptString param.Opt[string] `json:"opt_string"` + OptBool param.Opt[bool] `json:"opt_bool"` + + paramObject +} + +func (o *BasicObject) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, o) } + +func TestBasicObjectWithNull(t *testing.T) { + raw := `{"opt_int":null,"opt_string":null,"opt_bool":null}` + var dst BasicObject + target := BasicObject{ + OptInt: param.Null[int64](), + // OptFloat: param.Opt[float64]{}, + OptString: param.Null[string](), + OptBool: param.Null[bool](), + } + + err := json.Unmarshal([]byte(raw), &dst) + if err != nil { + t.Fatalf("failed unmarshal") + } + + if !reflect.DeepEqual(dst, target) { + t.Fatalf("failed equality check %#v", dst) + } +} + +func TestBasicObject(t *testing.T) { + raw := `{"req_int":1,"req_float":1.3,"req_string":"test","req_bool":true,"opt_int":2,"opt_float":2.0,"opt_string":"test","opt_bool":false}` + var dst BasicObject + target := BasicObject{ + ReqInt: 1, + ReqFloat: 1.3, + ReqString: "test", + ReqBool: true, + OptInt: param.NewOpt[int64](2), + OptFloat: param.NewOpt(2.0), + OptString: param.NewOpt("test"), + OptBool: param.NewOpt(false), + } + + err := json.Unmarshal([]byte(raw), &dst) + if err != nil { + t.Fatalf("failed unmarshal") + } + + if !reflect.DeepEqual(dst, target) { + t.Fatalf("failed equality check %#v", dst) + } +} + +type ComplexObject struct { + Basic BasicObject `json:"basic,required"` + Enum string `json:"enum"` + paramObject +} + +func (o *ComplexObject) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, o) } + +func init() { + apijson.RegisterFieldValidator[ComplexObject]("enum", "a", "b", "c") +} + +func TestComplexObject(t *testing.T) { + raw := `{"basic":{"req_int":1,"req_float":1.3,"req_string":"test","req_bool":true,"opt_int":2,"opt_float":2.0,"opt_string":"test","opt_bool":false},"enum":"a"}` + var dst ComplexObject + + target := ComplexObject{ + Basic: BasicObject{ + ReqInt: 1, + ReqFloat: 1.3, + ReqString: "test", + ReqBool: true, + OptInt: param.NewOpt[int64](2), + OptFloat: param.NewOpt(2.0), + OptString: param.NewOpt("test"), + OptBool: param.NewOpt(false), + }, + Enum: "a", + } + + err := json.Unmarshal([]byte(raw), &dst) + if err != nil { + t.Fatalf("failed unmarshal") + } + + if !reflect.DeepEqual(dst, target) { + t.Fatalf("failed equality check %#v", dst) + } +} + +type paramUnion = param.APIUnion + +type MemberA struct { + Name string `json:"name,required"` + Age int `json:"age,required"` +} + +type MemberB struct { + Name string `json:"name,required"` + Age string `json:"age,required"` +} + +type MemberC struct { + Name string `json:"name,required"` + Age int `json:"age,required"` + Status string `json:"status"` +} + +type MemberD struct { + Cost int `json:"cost,required"` + Status string `json:"status,required"` +} + +type MemberE struct { + Cost int `json:"cost,required"` + Status string `json:"status,required"` +} + +type MemberF struct { + D int `json:"d"` + E string `json:"e"` + F float64 `json:"f"` + G param.Opt[int] `json:"g"` +} + +type MemberG struct { + D int `json:"d"` + E string `json:"e"` + F float64 `json:"f"` + G param.Opt[bool] `json:"g"` +} + +func init() { + apijson.RegisterFieldValidator[MemberD]("status", "good", "ok", "bad") + apijson.RegisterFieldValidator[MemberE]("status", "GOOD", "OK", "BAD") +} + +type UnionStruct struct { + OfMemberA *MemberA `json:",inline"` + OfMemberB *MemberB `json:",inline"` + OfMemberC *MemberC `json:",inline"` + OfMemberD *MemberD `json:",inline"` + OfMemberE *MemberE `json:",inline"` + OfMemberF *MemberF `json:",inline"` + OfMemberG *MemberG `json:",inline"` + OfString param.Opt[string] `json:",inline"` + + paramUnion +} + +func (union *UnionStruct) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, union) +} + +func TestUnionStruct(t *testing.T) { + tests := map[string]struct { + raw string + target UnionStruct + shouldFail bool + }{ + "fail": { + raw: `1200`, + target: UnionStruct{}, + shouldFail: true, + }, + "easy": { + raw: `{"age":30}`, + target: UnionStruct{OfMemberA: &MemberA{Age: 30}}, + }, + "less-easy": { + raw: `{"age":"thirty"}`, + target: UnionStruct{OfMemberB: &MemberB{Age: "thirty"}}, + }, + "even-less-easy": { + raw: `{"age":"30"}`, + target: UnionStruct{OfMemberB: &MemberB{Age: "30"}}, + }, + "medium": { + raw: `{"name":"jacob","age":30}`, + target: UnionStruct{OfMemberA: &MemberA{ + Age: 30, + Name: "jacob", + }}, + }, + "less-medium": { + raw: `{"name":"jacob","age":"thirty"}`, + target: UnionStruct{OfMemberB: &MemberB{ + Age: "thirty", + Name: "jacob", + }}, + }, + "even-less-medium": { + raw: `{"name":"jacob","age":"30"}`, + target: UnionStruct{OfMemberB: &MemberB{ + Name: "jacob", + Age: "30", + }}, + }, + "hard": { + raw: `{"name":"jacob","age":30,"status":"active"}`, + target: UnionStruct{OfMemberC: &MemberC{ + Name: "jacob", + Age: 30, + Status: "active", + }}, + }, + "inline-string": { + raw: `"hello there"`, + target: UnionStruct{OfString: param.NewOpt("hello there")}, + }, + "enum-field": { + raw: `{"cost":100,"status":"ok"}`, + target: UnionStruct{OfMemberD: &MemberD{Cost: 100, Status: "ok"}}, + }, + "other-enum-field": { + raw: `{"cost":100,"status":"GOOD"}`, + target: UnionStruct{OfMemberE: &MemberE{Cost: 100, Status: "GOOD"}}, + }, + "tricky-extra-fields": { + raw: `{"d":12,"e":"hello","f":1.00}`, + target: UnionStruct{OfMemberF: &MemberF{D: 12, E: "hello", F: 1.00}}, + }, + "optional-fields": { + raw: `{"d":12,"e":"hello","f":1.00,"g":12}`, + target: UnionStruct{OfMemberF: &MemberF{D: 12, E: "hello", F: 1.00, G: param.NewOpt(12)}}, + }, + "optional-fields-2": { + raw: `{"d":12,"e":"hello","f":1.00,"g":false}`, + target: UnionStruct{OfMemberG: &MemberG{D: 12, E: "hello", F: 1.00, G: param.NewOpt(false)}}, + }, + } + + for name, test := range tests { + var dst UnionStruct + t.Run(name, func(t *testing.T) { + err := json.Unmarshal([]byte(test.raw), &dst) + if err != nil && !test.shouldFail { + t.Fatalf("failed unmarshal with err: %v %#v", err, dst) + } + + if !reflect.DeepEqual(dst, test.target) { + if dst.OfMemberA != nil { + fmt.Printf("%#v", dst.OfMemberA) + } + t.Fatalf("failed equality, got %#v but expected %#v", dst, test.target) + } + }) + } +} + +type ConstantA string +type ConstantB string +type ConstantC string + +func (c ConstantA) Default() string { return "A" } +func (c ConstantB) Default() string { return "B" } +func (c ConstantC) Default() string { return "C" } + +type DiscVariantA struct { + Name string `json:"name,required"` + Age int `json:"age,required"` + Type ConstantA `json:"type,required"` +} + +type DiscVariantB struct { + Name string `json:"name,required"` + Age int `json:"age,required"` + Type ConstantB `json:"type,required"` +} + +type DiscVariantC struct { + Name string `json:"name,required"` + Age float64 `json:"age,required"` + Type ConstantC `json:"type,required"` +} + +type DiscriminatedUnion struct { + OfA *DiscVariantA `json:",inline"` + OfB *DiscVariantB `json:",inline"` + OfC *DiscVariantC `json:",inline"` + + paramUnion +} + +func init() { + apijson.RegisterDiscriminatedUnion[DiscriminatedUnion]("type", map[string]reflect.Type{ + "A": reflect.TypeOf(DiscVariantA{}), + "B": reflect.TypeOf(DiscVariantB{}), + "C": reflect.TypeOf(DiscVariantC{}), + }) +} + +type FooVariant struct { + Type string `json:"type,required"` + Value string `json:"value,required"` +} + +type BarVariant struct { + Type string `json:"type,required"` + Enable bool `json:"enable,required"` +} + +type MultiDiscriminatorUnion struct { + OfFoo *FooVariant `json:",inline"` + OfBar *BarVariant `json:",inline"` + + paramUnion +} + +func init() { + apijson.RegisterDiscriminatedUnion[MultiDiscriminatorUnion]("type", map[string]reflect.Type{ + "foo": reflect.TypeOf(FooVariant{}), + "foo_v2": reflect.TypeOf(FooVariant{}), + "bar": reflect.TypeOf(BarVariant{}), + "bar_legacy": reflect.TypeOf(BarVariant{}), + }) +} + +func (m *MultiDiscriminatorUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, m) +} + +func (d *DiscriminatedUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, d) +} + +func TestDiscriminatedUnion(t *testing.T) { + tests := map[string]struct { + raw string + target DiscriminatedUnion + shouldFail bool + }{ + "variant_a": { + raw: `{"name":"Alice","age":25,"type":"A"}`, + target: DiscriminatedUnion{OfA: &DiscVariantA{ + Name: "Alice", + Age: 25, + Type: "A", + }}, + }, + "variant_b": { + raw: `{"name":"Bob","age":30,"type":"B"}`, + target: DiscriminatedUnion{OfB: &DiscVariantB{ + Name: "Bob", + Age: 30, + Type: "B", + }}, + }, + "variant_c": { + raw: `{"name":"Charlie","age":35.5,"type":"C"}`, + target: DiscriminatedUnion{OfC: &DiscVariantC{ + Name: "Charlie", + Age: 35.5, + Type: "C", + }}, + }, + "invalid_type": { + raw: `{"name":"Unknown","age":40,"type":"D"}`, + target: DiscriminatedUnion{}, + shouldFail: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + var dst DiscriminatedUnion + err := json.Unmarshal([]byte(test.raw), &dst) + if err != nil && !test.shouldFail { + t.Fatalf("failed unmarshal with err: %v", err) + } + if err == nil && test.shouldFail { + t.Fatalf("expected unmarshal to fail but it succeeded") + } + if !reflect.DeepEqual(dst, test.target) { + t.Fatalf("failed equality, got %#v but expected %#v", dst, test.target) + } + }) + } +} + +func TestMultiDiscriminatorUnion(t *testing.T) { + tests := map[string]struct { + raw string + target MultiDiscriminatorUnion + shouldFail bool + }{ + "foo_variant": { + raw: `{"type":"foo","value":"test"}`, + target: MultiDiscriminatorUnion{OfFoo: &FooVariant{ + Type: "foo", + Value: "test", + }}, + }, + "foo_v2_variant": { + raw: `{"type":"foo_v2","value":"test_v2"}`, + target: MultiDiscriminatorUnion{OfFoo: &FooVariant{ + Type: "foo_v2", + Value: "test_v2", + }}, + }, + "bar_variant": { + raw: `{"type":"bar","enable":true}`, + target: MultiDiscriminatorUnion{OfBar: &BarVariant{ + Type: "bar", + Enable: true, + }}, + }, + "bar_legacy_variant": { + raw: `{"type":"bar_legacy","enable":false}`, + target: MultiDiscriminatorUnion{OfBar: &BarVariant{ + Type: "bar_legacy", + Enable: false, + }}, + }, + "invalid_type": { + raw: `{"type":"unknown","value":"test"}`, + target: MultiDiscriminatorUnion{}, + shouldFail: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + var dst MultiDiscriminatorUnion + err := json.Unmarshal([]byte(test.raw), &dst) + if err != nil && !test.shouldFail { + t.Fatalf("failed unmarshal with err: %v", err) + } + if err == nil && test.shouldFail { + t.Fatalf("expected unmarshal to fail but it succeeded") + } + if !reflect.DeepEqual(dst, test.target) { + t.Fatalf("failed equality, got %#v but expected %#v", dst, test.target) + } + }) + } +} diff --git a/internal/apijson/decoder.go b/internal/apijson/decoder.go new file mode 100644 index 0000000..fd5046a --- /dev/null +++ b/internal/apijson/decoder.go @@ -0,0 +1,691 @@ +// The deserialization algorithm from apijson may be subject to improvements +// between minor versions, particularly with respect to calling [json.Unmarshal] +// into param unions. + +package apijson + +import ( + "encoding/json" + "fmt" + "github.com/imagekit-developer/imagekit-go/packages/param" + "reflect" + "strconv" + "sync" + "time" + "unsafe" + + "github.com/tidwall/gjson" +) + +// decoders is a synchronized map with roughly the following type: +// map[reflect.Type]decoderFunc +var decoders sync.Map + +// Unmarshal is similar to [encoding/json.Unmarshal] and parses the JSON-encoded +// data and stores it in the given pointer. +func Unmarshal(raw []byte, to any) error { + d := &decoderBuilder{dateFormat: time.RFC3339} + return d.unmarshal(raw, to) +} + +// UnmarshalRoot is like Unmarshal, but doesn't try to call MarshalJSON on the +// root element. Useful if a struct's UnmarshalJSON is overrode to use the +// behavior of this encoder versus the standard library. +func UnmarshalRoot(raw []byte, to any) error { + d := &decoderBuilder{dateFormat: time.RFC3339, root: true} + return d.unmarshal(raw, to) +} + +// decoderBuilder contains the 'compile-time' state of the decoder. +type decoderBuilder struct { + // Whether or not this is the first element and called by [UnmarshalRoot], see + // the documentation there to see why this is necessary. + root bool + // The dateFormat (a format string for [time.Format]) which is chosen by the + // last struct tag that was seen. + dateFormat string +} + +// decoderState contains the 'run-time' state of the decoder. +type decoderState struct { + strict bool + exactness exactness + validator *validationEntry +} + +// Exactness refers to how close to the type the result was if deserialization +// was successful. This is useful in deserializing unions, where you want to try +// each entry, first with strict, then with looser validation, without actually +// having to do a lot of redundant work by marshalling twice (or maybe even more +// times). +type exactness int8 + +const ( + // Some values had to fudged a bit, for example by converting a string to an + // int, or an enum with extra values. + loose exactness = iota + // There are some extra arguments, but other wise it matches the union. + extras + // Exactly right. + exact +) + +type decoderFunc func(node gjson.Result, value reflect.Value, state *decoderState) error + +type decoderField struct { + tag parsedStructTag + fn decoderFunc + idx []int + goname string +} + +type decoderEntry struct { + reflect.Type + dateFormat string + root bool +} + +func (d *decoderBuilder) unmarshal(raw []byte, to any) error { + value := reflect.ValueOf(to).Elem() + result := gjson.ParseBytes(raw) + if !value.IsValid() { + return fmt.Errorf("apijson: cannot marshal into invalid value") + } + return d.typeDecoder(value.Type())(result, value, &decoderState{strict: false, exactness: exact}) +} + +// unmarshalWithExactness is used for internal testing purposes. +func (d *decoderBuilder) unmarshalWithExactness(raw []byte, to any) (exactness, error) { + value := reflect.ValueOf(to).Elem() + result := gjson.ParseBytes(raw) + if !value.IsValid() { + return 0, fmt.Errorf("apijson: cannot marshal into invalid value") + } + state := decoderState{strict: false, exactness: exact} + err := d.typeDecoder(value.Type())(result, value, &state) + return state.exactness, err +} + +func (d *decoderBuilder) typeDecoder(t reflect.Type) decoderFunc { + entry := decoderEntry{ + Type: t, + dateFormat: d.dateFormat, + root: d.root, + } + + if fi, ok := decoders.Load(entry); ok { + return fi.(decoderFunc) + } + + // To deal with recursive types, populate the map with an + // indirect func before we build it. This type waits on the + // real func (f) to be ready and then calls it. This indirect + // func is only used for recursive types. + var ( + wg sync.WaitGroup + f decoderFunc + ) + wg.Add(1) + fi, loaded := decoders.LoadOrStore(entry, decoderFunc(func(node gjson.Result, v reflect.Value, state *decoderState) error { + wg.Wait() + return f(node, v, state) + })) + if loaded { + return fi.(decoderFunc) + } + + // Compute the real decoder and replace the indirect func with it. + f = d.newTypeDecoder(t) + wg.Done() + decoders.Store(entry, f) + return f +} + +// validatedTypeDecoder wraps the type decoder with a validator. This is helpful +// for ensuring that enum fields are correct. +func (d *decoderBuilder) validatedTypeDecoder(t reflect.Type, entry *validationEntry) decoderFunc { + dec := d.typeDecoder(t) + if entry == nil { + return dec + } + + // Thread the current validation entry through the decoder, + // but clean up in time for the next field. + return func(node gjson.Result, v reflect.Value, state *decoderState) error { + state.validator = entry + err := dec(node, v, state) + state.validator = nil + return err + } +} + +func indirectUnmarshalerDecoder(n gjson.Result, v reflect.Value, state *decoderState) error { + return v.Addr().Interface().(json.Unmarshaler).UnmarshalJSON([]byte(n.Raw)) +} + +func unmarshalerDecoder(n gjson.Result, v reflect.Value, state *decoderState) error { + if v.Kind() == reflect.Pointer && v.CanSet() { + v.Set(reflect.New(v.Type().Elem())) + } + return v.Interface().(json.Unmarshaler).UnmarshalJSON([]byte(n.Raw)) +} + +func (d *decoderBuilder) newTypeDecoder(t reflect.Type) decoderFunc { + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + return d.newTimeTypeDecoder(t) + } + + if t.Implements(reflect.TypeOf((*param.Optional)(nil)).Elem()) { + return d.newOptTypeDecoder(t) + } + + if !d.root && t.Implements(reflect.TypeOf((*json.Unmarshaler)(nil)).Elem()) { + return unmarshalerDecoder + } + if !d.root && reflect.PointerTo(t).Implements(reflect.TypeOf((*json.Unmarshaler)(nil)).Elem()) { + if _, ok := unionVariants[t]; !ok { + return indirectUnmarshalerDecoder + } + } + d.root = false + + if _, ok := unionRegistry[t]; ok { + if isStructUnion(t) { + return d.newStructUnionDecoder(t) + } + return d.newUnionDecoder(t) + } + + switch t.Kind() { + case reflect.Pointer: + inner := t.Elem() + innerDecoder := d.typeDecoder(inner) + + return func(n gjson.Result, v reflect.Value, state *decoderState) error { + if !v.IsValid() { + return fmt.Errorf("apijson: unexpected invalid reflection value %+#v", v) + } + + newValue := reflect.New(inner).Elem() + err := innerDecoder(n, newValue, state) + if err != nil { + return err + } + + v.Set(newValue.Addr()) + return nil + } + case reflect.Struct: + if isStructUnion(t) { + return d.newStructUnionDecoder(t) + } + return d.newStructTypeDecoder(t) + case reflect.Array: + fallthrough + case reflect.Slice: + return d.newArrayTypeDecoder(t) + case reflect.Map: + return d.newMapDecoder(t) + case reflect.Interface: + return func(node gjson.Result, value reflect.Value, state *decoderState) error { + if !value.IsValid() { + return fmt.Errorf("apijson: unexpected invalid value %+#v", value) + } + if node.Value() != nil && value.CanSet() { + value.Set(reflect.ValueOf(node.Value())) + } + return nil + } + default: + return d.newPrimitiveTypeDecoder(t) + } +} + +func (d *decoderBuilder) newMapDecoder(t reflect.Type) decoderFunc { + keyType := t.Key() + itemType := t.Elem() + itemDecoder := d.typeDecoder(itemType) + + return func(node gjson.Result, value reflect.Value, state *decoderState) (err error) { + mapValue := reflect.MakeMapWithSize(t, len(node.Map())) + + node.ForEach(func(key, value gjson.Result) bool { + // It's fine for us to just use `ValueOf` here because the key types will + // always be primitive types so we don't need to decode it using the standard pattern + keyValue := reflect.ValueOf(key.Value()) + if !keyValue.IsValid() { + if err == nil { + err = fmt.Errorf("apijson: received invalid key type %v", keyValue.String()) + } + return false + } + if keyValue.Type() != keyType { + if err == nil { + err = fmt.Errorf("apijson: expected key type %v but got %v", keyType, keyValue.Type()) + } + return false + } + + itemValue := reflect.New(itemType).Elem() + itemerr := itemDecoder(value, itemValue, state) + if itemerr != nil { + if err == nil { + err = itemerr + } + return false + } + + mapValue.SetMapIndex(keyValue, itemValue) + return true + }) + + if err != nil { + return err + } + value.Set(mapValue) + return nil + } +} + +func (d *decoderBuilder) newArrayTypeDecoder(t reflect.Type) decoderFunc { + itemDecoder := d.typeDecoder(t.Elem()) + + return func(node gjson.Result, value reflect.Value, state *decoderState) (err error) { + if !node.IsArray() { + return fmt.Errorf("apijson: could not deserialize to an array") + } + + arrayNode := node.Array() + + arrayValue := reflect.MakeSlice(reflect.SliceOf(t.Elem()), len(arrayNode), len(arrayNode)) + for i, itemNode := range arrayNode { + err = itemDecoder(itemNode, arrayValue.Index(i), state) + if err != nil { + return err + } + } + + value.Set(arrayValue) + return nil + } +} + +func (d *decoderBuilder) newStructTypeDecoder(t reflect.Type) decoderFunc { + // map of json field name to struct field decoders + decoderFields := map[string]decoderField{} + anonymousDecoders := []decoderField{} + extraDecoder := (*decoderField)(nil) + var inlineDecoders []decoderField + + validationEntries := validationRegistry[t] + + for i := 0; i < t.NumField(); i++ { + idx := []int{i} + field := t.FieldByIndex(idx) + if !field.IsExported() { + continue + } + + var validator *validationEntry + for _, entry := range validationEntries { + if entry.field.Offset == field.Offset { + validator = &entry + break + } + } + + // If this is an embedded struct, traverse one level deeper to extract + // the fields and get their encoders as well. + if field.Anonymous { + anonymousDecoders = append(anonymousDecoders, decoderField{ + fn: d.typeDecoder(field.Type), + idx: idx[:], + }) + continue + } + // If json tag is not present, then we skip, which is intentionally + // different behavior from the stdlib. + ptag, ok := parseJSONStructTag(field) + if !ok { + continue + } + // We only want to support unexported fields if they're tagged with + // `extras` because that field shouldn't be part of the public API. + if ptag.extras { + extraDecoder = &decoderField{ptag, d.typeDecoder(field.Type.Elem()), idx, field.Name} + continue + } + if ptag.inline { + df := decoderField{ptag, d.typeDecoder(field.Type), idx, field.Name} + inlineDecoders = append(inlineDecoders, df) + continue + } + if ptag.metadata { + continue + } + + oldFormat := d.dateFormat + dateFormat, ok := parseFormatStructTag(field) + if ok { + switch dateFormat { + case "date-time": + d.dateFormat = time.RFC3339 + case "date": + d.dateFormat = "2006-01-02" + } + } + + decoderFields[ptag.name] = decoderField{ + ptag, + d.validatedTypeDecoder(field.Type, validator), + idx, field.Name, + } + + d.dateFormat = oldFormat + } + + return func(node gjson.Result, value reflect.Value, state *decoderState) (err error) { + if field := value.FieldByName("JSON"); field.IsValid() { + if raw := field.FieldByName("raw"); raw.IsValid() { + setUnexportedField(raw, node.Raw) + } + } + + for _, decoder := range anonymousDecoders { + // ignore errors + decoder.fn(node, value.FieldByIndex(decoder.idx), state) + } + + for _, inlineDecoder := range inlineDecoders { + var meta Field + dest := value.FieldByIndex(inlineDecoder.idx) + isValid := false + if dest.IsValid() && node.Type != gjson.Null { + inlineState := decoderState{exactness: state.exactness, strict: true} + err = inlineDecoder.fn(node, dest, &inlineState) + if err == nil { + isValid = true + } + } + + if node.Type == gjson.Null { + meta = Field{ + raw: node.Raw, + status: null, + } + } else if !isValid { + // If an inline decoder fails, unset the field and move on. + if dest.IsValid() { + dest.SetZero() + } + continue + } else if isValid { + meta = Field{ + raw: node.Raw, + status: valid, + } + } + setMetadataSubField(value, inlineDecoder.idx, inlineDecoder.goname, meta) + } + + typedExtraType := reflect.Type(nil) + typedExtraFields := reflect.Value{} + if extraDecoder != nil { + typedExtraType = value.FieldByIndex(extraDecoder.idx).Type() + typedExtraFields = reflect.MakeMap(typedExtraType) + } + untypedExtraFields := map[string]Field{} + + for fieldName, itemNode := range node.Map() { + df, explicit := decoderFields[fieldName] + var ( + dest reflect.Value + fn decoderFunc + meta Field + ) + if explicit { + fn = df.fn + dest = value.FieldByIndex(df.idx) + } + if !explicit && extraDecoder != nil { + dest = reflect.New(typedExtraType.Elem()).Elem() + fn = extraDecoder.fn + } + + isValid := false + if dest.IsValid() && itemNode.Type != gjson.Null { + err = fn(itemNode, dest, state) + if err == nil { + isValid = true + } + } + + // Handle null [param.Opt] + if itemNode.Type == gjson.Null && dest.IsValid() && dest.Type().Implements(reflect.TypeOf((*param.Optional)(nil)).Elem()) { + dest.Addr().Interface().(json.Unmarshaler).UnmarshalJSON([]byte(itemNode.Raw)) + continue + } + + if itemNode.Type == gjson.Null { + meta = Field{ + raw: itemNode.Raw, + status: null, + } + } else if !isValid { + meta = Field{ + raw: itemNode.Raw, + status: invalid, + } + } else if isValid { + meta = Field{ + raw: itemNode.Raw, + status: valid, + } + } + + if explicit { + setMetadataSubField(value, df.idx, df.goname, meta) + } + if !explicit { + untypedExtraFields[fieldName] = meta + } + if !explicit && extraDecoder != nil { + typedExtraFields.SetMapIndex(reflect.ValueOf(fieldName), dest) + } + } + + if extraDecoder != nil && typedExtraFields.Len() > 0 { + value.FieldByIndex(extraDecoder.idx).Set(typedExtraFields) + } + + // Set exactness to 'extras' if there are untyped, extra fields. + if len(untypedExtraFields) > 0 && state.exactness > extras { + state.exactness = extras + } + + if len(untypedExtraFields) > 0 { + setMetadataExtraFields(value, []int{-1}, "ExtraFields", untypedExtraFields) + } + return nil + } +} + +func (d *decoderBuilder) newPrimitiveTypeDecoder(t reflect.Type) decoderFunc { + switch t.Kind() { + case reflect.String: + return func(n gjson.Result, v reflect.Value, state *decoderState) error { + v.SetString(n.String()) + if guardStrict(state, n.Type != gjson.String) { + return fmt.Errorf("apijson: failed to parse string strictly") + } + // Everything that is not an object can be loosely stringified. + if n.Type == gjson.JSON { + return fmt.Errorf("apijson: failed to parse string") + } + + state.validateString(v) + + if guardUnknown(state, v) { + return fmt.Errorf("apijson: failed string enum validation") + } + return nil + } + case reflect.Bool: + return func(n gjson.Result, v reflect.Value, state *decoderState) error { + v.SetBool(n.Bool()) + if guardStrict(state, n.Type != gjson.True && n.Type != gjson.False) { + return fmt.Errorf("apijson: failed to parse bool strictly") + } + // Numbers and strings that are either 'true' or 'false' can be loosely + // deserialized as bool. + if n.Type == gjson.String && (n.Raw != "true" && n.Raw != "false") || n.Type == gjson.JSON { + return fmt.Errorf("apijson: failed to parse bool") + } + + state.validateBool(v) + + if guardUnknown(state, v) { + return fmt.Errorf("apijson: failed bool enum validation") + } + return nil + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return func(n gjson.Result, v reflect.Value, state *decoderState) error { + v.SetInt(n.Int()) + if guardStrict(state, n.Type != gjson.Number || n.Num != float64(int(n.Num))) { + return fmt.Errorf("apijson: failed to parse int strictly") + } + // Numbers, booleans, and strings that maybe look like numbers can be + // loosely deserialized as numbers. + if n.Type == gjson.JSON || (n.Type == gjson.String && !canParseAsNumber(n.Str)) { + return fmt.Errorf("apijson: failed to parse int") + } + + state.validateInt(v) + + if guardUnknown(state, v) { + return fmt.Errorf("apijson: failed int enum validation") + } + return nil + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return func(n gjson.Result, v reflect.Value, state *decoderState) error { + v.SetUint(n.Uint()) + if guardStrict(state, n.Type != gjson.Number || n.Num != float64(int(n.Num)) || n.Num < 0) { + return fmt.Errorf("apijson: failed to parse uint strictly") + } + // Numbers, booleans, and strings that maybe look like numbers can be + // loosely deserialized as uint. + if n.Type == gjson.JSON || (n.Type == gjson.String && !canParseAsNumber(n.Str)) { + return fmt.Errorf("apijson: failed to parse uint") + } + if guardUnknown(state, v) { + return fmt.Errorf("apijson: failed uint enum validation") + } + return nil + } + case reflect.Float32, reflect.Float64: + return func(n gjson.Result, v reflect.Value, state *decoderState) error { + v.SetFloat(n.Float()) + if guardStrict(state, n.Type != gjson.Number) { + return fmt.Errorf("apijson: failed to parse float strictly") + } + // Numbers, booleans, and strings that maybe look like numbers can be + // loosely deserialized as floats. + if n.Type == gjson.JSON || (n.Type == gjson.String && !canParseAsNumber(n.Str)) { + return fmt.Errorf("apijson: failed to parse float") + } + if guardUnknown(state, v) { + return fmt.Errorf("apijson: failed float enum validation") + } + return nil + } + default: + return func(node gjson.Result, v reflect.Value, state *decoderState) error { + return fmt.Errorf("unknown type received at primitive decoder: %s", t.String()) + } + } +} + +func (d *decoderBuilder) newOptTypeDecoder(t reflect.Type) decoderFunc { + for t.Kind() == reflect.Pointer { + t = t.Elem() + } + valueField, _ := t.FieldByName("Value") + return func(n gjson.Result, v reflect.Value, state *decoderState) error { + state.validateOptKind(n, valueField.Type) + return v.Addr().Interface().(json.Unmarshaler).UnmarshalJSON([]byte(n.Raw)) + } +} + +func (d *decoderBuilder) newTimeTypeDecoder(t reflect.Type) decoderFunc { + format := d.dateFormat + return func(n gjson.Result, v reflect.Value, state *decoderState) error { + parsed, err := time.Parse(format, n.Str) + if err == nil { + v.Set(reflect.ValueOf(parsed).Convert(t)) + return nil + } + + if guardStrict(state, true) { + return err + } + + layouts := []string{ + "2006-01-02", + "2006-01-02T15:04:05Z07:00", + "2006-01-02T15:04:05Z0700", + "2006-01-02T15:04:05", + "2006-01-02 15:04:05Z07:00", + "2006-01-02 15:04:05Z0700", + "2006-01-02 15:04:05", + } + + for _, layout := range layouts { + parsed, err := time.Parse(layout, n.Str) + if err == nil { + v.Set(reflect.ValueOf(parsed).Convert(t)) + return nil + } + } + + return fmt.Errorf("unable to leniently parse date-time string: %s", n.Str) + } +} + +func setUnexportedField(field reflect.Value, value any) { + reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Set(reflect.ValueOf(value)) +} + +func guardStrict(state *decoderState, cond bool) bool { + if !cond { + return false + } + + if state.strict { + return true + } + + state.exactness = loose + return false +} + +func canParseAsNumber(str string) bool { + _, err := strconv.ParseFloat(str, 64) + return err == nil +} + +var stringType = reflect.TypeOf(string("")) + +func guardUnknown(state *decoderState, v reflect.Value) bool { + if have, ok := v.Interface().(interface{ IsKnown() bool }); guardStrict(state, ok && !have.IsKnown()) { + return true + } + + constantString, ok := v.Interface().(interface{ Default() string }) + named := v.Type() != stringType + if guardStrict(state, ok && named && v.Equal(reflect.ValueOf(constantString.Default()))) { + return true + } + return false +} diff --git a/internal/apijson/decoderesp_test.go b/internal/apijson/decoderesp_test.go new file mode 100644 index 0000000..bc75f46 --- /dev/null +++ b/internal/apijson/decoderesp_test.go @@ -0,0 +1,30 @@ +package apijson_test + +import ( + "encoding/json" + "github.com/imagekit-developer/imagekit-go/internal/apijson" + "github.com/imagekit-developer/imagekit-go/packages/respjson" + "testing" +) + +type StructWithNullExtraField struct { + Results []string `json:"results,required"` + JSON struct { + Results respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +func (r *StructWithNullExtraField) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +func TestDecodeWithNullExtraField(t *testing.T) { + raw := `{"something_else":null}` + var dst *StructWithNullExtraField + err := json.Unmarshal([]byte(raw), &dst) + if err != nil { + t.Fatalf("error: %s", err.Error()) + } +} diff --git a/internal/apijson/encoder.go b/internal/apijson/encoder.go new file mode 100644 index 0000000..8358a2f --- /dev/null +++ b/internal/apijson/encoder.go @@ -0,0 +1,392 @@ +package apijson + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/tidwall/sjson" +) + +var encoders sync.Map // map[encoderEntry]encoderFunc + +func Marshal(value any) ([]byte, error) { + e := &encoder{dateFormat: time.RFC3339} + return e.marshal(value) +} + +func MarshalRoot(value any) ([]byte, error) { + e := &encoder{root: true, dateFormat: time.RFC3339} + return e.marshal(value) +} + +type encoder struct { + dateFormat string + root bool +} + +type encoderFunc func(value reflect.Value) ([]byte, error) + +type encoderField struct { + tag parsedStructTag + fn encoderFunc + idx []int +} + +type encoderEntry struct { + reflect.Type + dateFormat string + root bool +} + +func (e *encoder) marshal(value any) ([]byte, error) { + val := reflect.ValueOf(value) + if !val.IsValid() { + return nil, nil + } + typ := val.Type() + enc := e.typeEncoder(typ) + return enc(val) +} + +func (e *encoder) typeEncoder(t reflect.Type) encoderFunc { + entry := encoderEntry{ + Type: t, + dateFormat: e.dateFormat, + root: e.root, + } + + if fi, ok := encoders.Load(entry); ok { + return fi.(encoderFunc) + } + + // To deal with recursive types, populate the map with an + // indirect func before we build it. This type waits on the + // real func (f) to be ready and then calls it. This indirect + // func is only used for recursive types. + var ( + wg sync.WaitGroup + f encoderFunc + ) + wg.Add(1) + fi, loaded := encoders.LoadOrStore(entry, encoderFunc(func(v reflect.Value) ([]byte, error) { + wg.Wait() + return f(v) + })) + if loaded { + return fi.(encoderFunc) + } + + // Compute the real encoder and replace the indirect func with it. + f = e.newTypeEncoder(t) + wg.Done() + encoders.Store(entry, f) + return f +} + +func marshalerEncoder(v reflect.Value) ([]byte, error) { + return v.Interface().(json.Marshaler).MarshalJSON() +} + +func indirectMarshalerEncoder(v reflect.Value) ([]byte, error) { + return v.Addr().Interface().(json.Marshaler).MarshalJSON() +} + +func (e *encoder) newTypeEncoder(t reflect.Type) encoderFunc { + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + return e.newTimeTypeEncoder() + } + if !e.root && t.Implements(reflect.TypeOf((*json.Marshaler)(nil)).Elem()) { + return marshalerEncoder + } + if !e.root && reflect.PointerTo(t).Implements(reflect.TypeOf((*json.Marshaler)(nil)).Elem()) { + return indirectMarshalerEncoder + } + e.root = false + switch t.Kind() { + case reflect.Pointer: + inner := t.Elem() + + innerEncoder := e.typeEncoder(inner) + return func(v reflect.Value) ([]byte, error) { + if !v.IsValid() || v.IsNil() { + return nil, nil + } + return innerEncoder(v.Elem()) + } + case reflect.Struct: + return e.newStructTypeEncoder(t) + case reflect.Array: + fallthrough + case reflect.Slice: + return e.newArrayTypeEncoder(t) + case reflect.Map: + return e.newMapEncoder(t) + case reflect.Interface: + return e.newInterfaceEncoder() + default: + return e.newPrimitiveTypeEncoder(t) + } +} + +func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc { + switch t.Kind() { + // Note that we could use `gjson` to encode these types but it would complicate our + // code more and this current code shouldn't cause any issues + case reflect.String: + return func(v reflect.Value) ([]byte, error) { + return json.Marshal(v.Interface()) + } + case reflect.Bool: + return func(v reflect.Value) ([]byte, error) { + if v.Bool() { + return []byte("true"), nil + } + return []byte("false"), nil + } + case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64: + return func(v reflect.Value) ([]byte, error) { + return []byte(strconv.FormatInt(v.Int(), 10)), nil + } + case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return func(v reflect.Value) ([]byte, error) { + return []byte(strconv.FormatUint(v.Uint(), 10)), nil + } + case reflect.Float32: + return func(v reflect.Value) ([]byte, error) { + return []byte(strconv.FormatFloat(v.Float(), 'f', -1, 32)), nil + } + case reflect.Float64: + return func(v reflect.Value) ([]byte, error) { + return []byte(strconv.FormatFloat(v.Float(), 'f', -1, 64)), nil + } + default: + return func(v reflect.Value) ([]byte, error) { + return nil, fmt.Errorf("unknown type received at primitive encoder: %s", t.String()) + } + } +} + +func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc { + itemEncoder := e.typeEncoder(t.Elem()) + + return func(value reflect.Value) ([]byte, error) { + json := []byte("[]") + for i := 0; i < value.Len(); i++ { + var value, err = itemEncoder(value.Index(i)) + if err != nil { + return nil, err + } + if value == nil { + // Assume that empty items should be inserted as `null` so that the output array + // will be the same length as the input array + value = []byte("null") + } + + json, err = sjson.SetRawBytes(json, "-1", value) + if err != nil { + return nil, err + } + } + + return json, nil + } +} + +func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { + encoderFields := []encoderField{} + extraEncoder := (*encoderField)(nil) + + // This helper allows us to recursively collect field encoders into a flat + // array. The parameter `index` keeps track of the access patterns necessary + // to get to some field. + var collectEncoderFields func(r reflect.Type, index []int) + collectEncoderFields = func(r reflect.Type, index []int) { + for i := 0; i < r.NumField(); i++ { + idx := append(index, i) + field := t.FieldByIndex(idx) + if !field.IsExported() { + continue + } + // If this is an embedded struct, traverse one level deeper to extract + // the field and get their encoders as well. + if field.Anonymous { + collectEncoderFields(field.Type, idx) + continue + } + // If json tag is not present, then we skip, which is intentionally + // different behavior from the stdlib. + ptag, ok := parseJSONStructTag(field) + if !ok { + continue + } + // We only want to support unexported field if they're tagged with + // `extras` because that field shouldn't be part of the public API. We + // also want to only keep the top level extras + if ptag.extras && len(index) == 0 { + extraEncoder = &encoderField{ptag, e.typeEncoder(field.Type.Elem()), idx} + continue + } + if ptag.name == "-" { + continue + } + + dateFormat, ok := parseFormatStructTag(field) + oldFormat := e.dateFormat + if ok { + switch dateFormat { + case "date-time": + e.dateFormat = time.RFC3339 + case "date": + e.dateFormat = "2006-01-02" + } + } + encoderFields = append(encoderFields, encoderField{ptag, e.typeEncoder(field.Type), idx}) + e.dateFormat = oldFormat + } + } + collectEncoderFields(t, []int{}) + + // Ensure deterministic output by sorting by lexicographic order + sort.Slice(encoderFields, func(i, j int) bool { + return encoderFields[i].tag.name < encoderFields[j].tag.name + }) + + return func(value reflect.Value) (json []byte, err error) { + json = []byte("{}") + + for _, ef := range encoderFields { + field := value.FieldByIndex(ef.idx) + encoded, err := ef.fn(field) + if err != nil { + return nil, err + } + if encoded == nil { + continue + } + json, err = sjson.SetRawBytes(json, ef.tag.name, encoded) + if err != nil { + return nil, err + } + } + + if extraEncoder != nil { + json, err = e.encodeMapEntries(json, value.FieldByIndex(extraEncoder.idx)) + if err != nil { + return nil, err + } + } + return + } +} + +func (e *encoder) newFieldTypeEncoder(t reflect.Type) encoderFunc { + f, _ := t.FieldByName("Value") + enc := e.typeEncoder(f.Type) + + return func(value reflect.Value) (json []byte, err error) { + present := value.FieldByName("Present") + if !present.Bool() { + return nil, nil + } + null := value.FieldByName("Null") + if null.Bool() { + return []byte("null"), nil + } + raw := value.FieldByName("Raw") + if !raw.IsNil() { + return e.typeEncoder(raw.Type())(raw) + } + return enc(value.FieldByName("Value")) + } +} + +func (e *encoder) newTimeTypeEncoder() encoderFunc { + format := e.dateFormat + return func(value reflect.Value) (json []byte, err error) { + return []byte(`"` + value.Convert(reflect.TypeOf(time.Time{})).Interface().(time.Time).Format(format) + `"`), nil + } +} + +func (e encoder) newInterfaceEncoder() encoderFunc { + return func(value reflect.Value) ([]byte, error) { + value = value.Elem() + if !value.IsValid() { + return nil, nil + } + return e.typeEncoder(value.Type())(value) + } +} + +// Given a []byte of json (may either be an empty object or an object that already contains entries) +// encode all of the entries in the map to the json byte array. +func (e *encoder) encodeMapEntries(json []byte, v reflect.Value) ([]byte, error) { + type mapPair struct { + key []byte + value reflect.Value + } + + pairs := []mapPair{} + keyEncoder := e.typeEncoder(v.Type().Key()) + + iter := v.MapRange() + for iter.Next() { + var encodedKeyString string + if iter.Key().Type().Kind() == reflect.String { + encodedKeyString = iter.Key().String() + } else { + var err error + encodedKeyBytes, err := keyEncoder(iter.Key()) + if err != nil { + return nil, err + } + encodedKeyString = string(encodedKeyBytes) + } + encodedKey := []byte(sjsonReplacer.Replace(encodedKeyString)) + pairs = append(pairs, mapPair{key: encodedKey, value: iter.Value()}) + } + + // Ensure deterministic output + sort.Slice(pairs, func(i, j int) bool { + return bytes.Compare(pairs[i].key, pairs[j].key) < 0 + }) + + elementEncoder := e.typeEncoder(v.Type().Elem()) + for _, p := range pairs { + encodedValue, err := elementEncoder(p.value) + if err != nil { + return nil, err + } + if len(encodedValue) == 0 { + continue + } + json, err = sjson.SetRawBytes(json, string(p.key), encodedValue) + if err != nil { + return nil, err + } + } + + return json, nil +} + +func (e *encoder) newMapEncoder(_ reflect.Type) encoderFunc { + return func(value reflect.Value) ([]byte, error) { + json := []byte("{}") + var err error + json, err = e.encodeMapEntries(json, value) + if err != nil { + return nil, err + } + return json, nil + } +} + +// If we want to set a literal key value into JSON using sjson, we need to make sure it doesn't have +// special characters that sjson interprets as a path. +var sjsonReplacer *strings.Replacer = strings.NewReplacer(".", "\\.", ":", "\\:", "*", "\\*") diff --git a/internal/apijson/enum.go b/internal/apijson/enum.go new file mode 100644 index 0000000..18b218a --- /dev/null +++ b/internal/apijson/enum.go @@ -0,0 +1,145 @@ +package apijson + +import ( + "fmt" + "reflect" + "slices" + "sync" + + "github.com/tidwall/gjson" +) + +/********************/ +/* Validating Enums */ +/********************/ + +type validationEntry struct { + field reflect.StructField + required bool + legalValues struct { + strings []string + // 1 represents true, 0 represents false, -1 represents either + bools int + ints []int64 + } +} + +type validatorFunc func(reflect.Value) exactness + +var validators sync.Map +var validationRegistry = map[reflect.Type][]validationEntry{} + +func RegisterFieldValidator[T any, V string | bool | int](fieldName string, values ...V) { + var t T + parentType := reflect.TypeOf(t) + + if _, ok := validationRegistry[parentType]; !ok { + validationRegistry[parentType] = []validationEntry{} + } + + // The following checks run at initialization time, + // it is impossible for them to panic if any tests pass. + if parentType.Kind() != reflect.Struct { + panic(fmt.Sprintf("apijson: cannot initialize validator for non-struct %s", parentType.String())) + } + + var field reflect.StructField + found := false + for i := 0; i < parentType.NumField(); i++ { + ptag, ok := parseJSONStructTag(parentType.Field(i)) + if ok && ptag.name == fieldName { + field = parentType.Field(i) + found = true + break + } + } + + if !found { + panic(fmt.Sprintf("apijson: cannot find field %s in struct %s", fieldName, parentType.String())) + } + + newEntry := validationEntry{field: field} + newEntry.legalValues.bools = -1 // default to either + + switch values := any(values).(type) { + case []string: + newEntry.legalValues.strings = values + case []int: + newEntry.legalValues.ints = make([]int64, len(values)) + for i, value := range values { + newEntry.legalValues.ints[i] = int64(value) + } + case []bool: + for i, value := range values { + var next int + if value { + next = 1 + } + if i > 0 && newEntry.legalValues.bools != next { + newEntry.legalValues.bools = -1 // accept either + break + } + newEntry.legalValues.bools = next + } + } + + // Store the information necessary to create a validator, so that we can use it + // lazily create the validator function when did. + validationRegistry[parentType] = append(validationRegistry[parentType], newEntry) +} + +func (state *decoderState) validateString(v reflect.Value) { + if state.validator == nil { + return + } + if !slices.Contains(state.validator.legalValues.strings, v.String()) { + state.exactness = loose + } +} + +func (state *decoderState) validateInt(v reflect.Value) { + if state.validator == nil { + return + } + if !slices.Contains(state.validator.legalValues.ints, v.Int()) { + state.exactness = loose + } +} + +func (state *decoderState) validateBool(v reflect.Value) { + if state.validator == nil { + return + } + b := v.Bool() + if state.validator.legalValues.bools == 1 && b == false { + state.exactness = loose + } else if state.validator.legalValues.bools == 0 && b == true { + state.exactness = loose + } +} + +func (state *decoderState) validateOptKind(node gjson.Result, t reflect.Type) { + switch node.Type { + case gjson.JSON: + state.exactness = loose + case gjson.Null: + return + case gjson.False, gjson.True: + if t.Kind() != reflect.Bool { + state.exactness = loose + } + case gjson.Number: + switch t.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + return + default: + state.exactness = loose + } + case gjson.String: + if t.Kind() != reflect.String { + state.exactness = loose + } + } +} diff --git a/internal/apijson/enum_test.go b/internal/apijson/enum_test.go new file mode 100644 index 0000000..a2aeed4 --- /dev/null +++ b/internal/apijson/enum_test.go @@ -0,0 +1,87 @@ +package apijson + +import ( + "reflect" + "testing" +) + +type EnumStruct struct { + NormalString string `json:"normal_string"` + StringEnum string `json:"string_enum"` + NamedEnum NamedEnumType `json:"named_enum"` + + IntEnum int `json:"int_enum"` + BoolEnum bool `json:"bool_enum"` + + WeirdBoolEnum bool `json:"weird_bool_enum"` +} + +func (o *EnumStruct) UnmarshalJSON(data []byte) error { + return UnmarshalRoot(data, o) +} + +func init() { + RegisterFieldValidator[EnumStruct]("string_enum", "one", "two", "three") + RegisterFieldValidator[EnumStruct]("int_enum", 200, 404) + RegisterFieldValidator[EnumStruct]("bool_enum", false) + RegisterFieldValidator[EnumStruct]("weird_bool_enum", true, false) +} + +type NamedEnumType string + +const ( + NamedEnumOne NamedEnumType = "one" + NamedEnumTwo NamedEnumType = "two" + NamedEnumThree NamedEnumType = "three" +) + +func (e NamedEnumType) IsKnown() bool { + return e == NamedEnumOne || e == NamedEnumTwo || e == NamedEnumThree +} + +func TestEnumStructStringValidator(t *testing.T) { + cases := map[string]struct { + exactness + EnumStruct + }{ + `{"string_enum":"one"}`: {exact, EnumStruct{StringEnum: "one"}}, + `{"string_enum":"two"}`: {exact, EnumStruct{StringEnum: "two"}}, + `{"string_enum":"three"}`: {exact, EnumStruct{StringEnum: "three"}}, + `{"string_enum":"none"}`: {loose, EnumStruct{StringEnum: "none"}}, + `{"int_enum":200}`: {exact, EnumStruct{IntEnum: 200}}, + `{"int_enum":404}`: {exact, EnumStruct{IntEnum: 404}}, + `{"int_enum":500}`: {loose, EnumStruct{IntEnum: 500}}, + `{"bool_enum":false}`: {exact, EnumStruct{BoolEnum: false}}, + `{"bool_enum":true}`: {loose, EnumStruct{BoolEnum: true}}, + `{"weird_bool_enum":true}`: {exact, EnumStruct{WeirdBoolEnum: true}}, + `{"weird_bool_enum":false}`: {exact, EnumStruct{WeirdBoolEnum: false}}, + + `{"named_enum":"one"}`: {exact, EnumStruct{NamedEnum: NamedEnumOne}}, + `{"named_enum":"none"}`: {loose, EnumStruct{NamedEnum: "none"}}, + + `{"string_enum":"one","named_enum":"one"}`: {exact, EnumStruct{NamedEnum: "one", StringEnum: "one"}}, + `{"string_enum":"four","named_enum":"one"}`: { + loose, + EnumStruct{NamedEnum: "one", StringEnum: "four"}, + }, + `{"string_enum":"one","named_enum":"four"}`: { + loose, EnumStruct{NamedEnum: "four", StringEnum: "one"}, + }, + `{"wrong_key":"one"}`: {extras, EnumStruct{StringEnum: ""}}, + } + + for raw, expected := range cases { + var dst EnumStruct + + dec := decoderBuilder{root: true} + exactness, _ := dec.unmarshalWithExactness([]byte(raw), &dst) + + if !reflect.DeepEqual(dst, expected.EnumStruct) { + t.Fatalf("failed equality check %#v", dst) + } + + if exactness != expected.exactness { + t.Fatalf("exactness got %d expected %d %s", exactness, expected.exactness, raw) + } + } +} diff --git a/internal/apijson/field.go b/internal/apijson/field.go new file mode 100644 index 0000000..854d6dd --- /dev/null +++ b/internal/apijson/field.go @@ -0,0 +1,23 @@ +package apijson + +type status uint8 + +const ( + missing status = iota + null + invalid + valid +) + +type Field struct { + raw string + status status +} + +// Returns true if the field is explicitly `null` _or_ if it is not present at all (ie, missing). +// To check if the field's key is present in the JSON with an explicit null value, +// you must check `f.IsNull() && !f.IsMissing()`. +func (j Field) IsNull() bool { return j.status <= null } +func (j Field) IsMissing() bool { return j.status == missing } +func (j Field) IsInvalid() bool { return j.status == invalid } +func (j Field) Raw() string { return j.raw } diff --git a/internal/apijson/json_test.go b/internal/apijson/json_test.go new file mode 100644 index 0000000..02904d2 --- /dev/null +++ b/internal/apijson/json_test.go @@ -0,0 +1,616 @@ +package apijson + +import ( + "reflect" + "strings" + "testing" + "time" + + "github.com/tidwall/gjson" +) + +func P[T any](v T) *T { return &v } + +type Primitives struct { + A bool `json:"a"` + B int `json:"b"` + C uint `json:"c"` + D float64 `json:"d"` + E float32 `json:"e"` + F []int `json:"f"` +} + +type PrimitivePointers struct { + A *bool `json:"a"` + B *int `json:"b"` + C *uint `json:"c"` + D *float64 `json:"d"` + E *float32 `json:"e"` + F *[]int `json:"f"` +} + +type Slices struct { + Slice []Primitives `json:"slices"` +} + +type DateTime struct { + Date time.Time `json:"date" format:"date"` + DateTime time.Time `json:"date-time" format:"date-time"` +} + +type AdditionalProperties struct { + A bool `json:"a"` + ExtraFields map[string]any `json:"-,extras"` +} + +type TypedAdditionalProperties struct { + A bool `json:"a"` + ExtraFields map[string]int `json:"-,extras"` +} + +type EmbeddedStruct struct { + A bool `json:"a"` + B string `json:"b"` + + JSON EmbeddedStructJSON +} + +type EmbeddedStructJSON struct { + A Field + B Field + ExtraFields map[string]Field + raw string +} + +type EmbeddedStructs struct { + EmbeddedStruct + A *int `json:"a"` + ExtraFields map[string]any `json:"-,extras"` + + JSON EmbeddedStructsJSON +} + +type EmbeddedStructsJSON struct { + A Field + ExtraFields map[string]Field + raw string +} + +type Recursive struct { + Name string `json:"name"` + Child *Recursive `json:"child"` +} + +type JSONFieldStruct struct { + A bool `json:"a"` + B int64 `json:"b"` + C string `json:"c"` + D string `json:"d"` + ExtraFields map[string]int64 `json:",extras"` + JSON JSONFieldStructJSON `json:",metadata"` +} + +type JSONFieldStructJSON struct { + A Field + B Field + C Field + D Field + ExtraFields map[string]Field + raw string +} + +type UnknownStruct struct { + Unknown any `json:"unknown"` +} + +type UnionStruct struct { + Union Union `json:"union" format:"date"` +} + +type Union interface { + union() +} + +type Inline struct { + InlineField Primitives `json:",inline"` + JSON InlineJSON `json:",metadata"` +} + +type InlineArray struct { + InlineField []string `json:",inline"` + JSON InlineJSON `json:",metadata"` +} + +type InlineJSON struct { + InlineField Field + raw string +} + +type UnionInteger int64 + +func (UnionInteger) union() {} + +type UnionStructA struct { + Type string `json:"type"` + A string `json:"a"` + B string `json:"b"` +} + +func (UnionStructA) union() {} + +type UnionStructB struct { + Type string `json:"type"` + A string `json:"a"` +} + +func (UnionStructB) union() {} + +type UnionTime time.Time + +func (UnionTime) union() {} + +func init() { + RegisterUnion[Union]("type", + UnionVariant{ + TypeFilter: gjson.String, + Type: reflect.TypeOf(UnionTime{}), + }, + UnionVariant{ + TypeFilter: gjson.Number, + Type: reflect.TypeOf(UnionInteger(0)), + }, + UnionVariant{ + TypeFilter: gjson.JSON, + DiscriminatorValue: "typeA", + Type: reflect.TypeOf(UnionStructA{}), + }, + UnionVariant{ + TypeFilter: gjson.JSON, + DiscriminatorValue: "typeB", + Type: reflect.TypeOf(UnionStructB{}), + }, + ) +} + +type ComplexUnionStruct struct { + Union ComplexUnion `json:"union"` +} + +type ComplexUnion interface { + complexUnion() +} + +type ComplexUnionA struct { + Boo string `json:"boo"` + Foo bool `json:"foo"` +} + +func (ComplexUnionA) complexUnion() {} + +type ComplexUnionB struct { + Boo bool `json:"boo"` + Foo string `json:"foo"` +} + +func (ComplexUnionB) complexUnion() {} + +type ComplexUnionC struct { + Boo int64 `json:"boo"` +} + +func (ComplexUnionC) complexUnion() {} + +type ComplexUnionTypeA struct { + Baz int64 `json:"baz"` + Type TypeA `json:"type"` +} + +func (ComplexUnionTypeA) complexUnion() {} + +type TypeA string + +func (t TypeA) IsKnown() bool { + return t == "a" +} + +type ComplexUnionTypeB struct { + Baz int64 `json:"baz"` + Type TypeB `json:"type"` +} + +type TypeB string + +func (t TypeB) IsKnown() bool { + return t == "b" +} + +type UnmarshalStruct struct { + Foo string `json:"foo"` + prop bool `json:"-"` +} + +func (r *UnmarshalStruct) UnmarshalJSON(json []byte) error { + r.prop = true + return UnmarshalRoot(json, r) +} + +func (ComplexUnionTypeB) complexUnion() {} + +func init() { + RegisterUnion[ComplexUnion]("", + UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(ComplexUnionA{}), + }, + UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(ComplexUnionB{}), + }, + UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(ComplexUnionC{}), + }, + UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(ComplexUnionTypeA{}), + }, + UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(ComplexUnionTypeB{}), + }, + ) +} + +type MarshallingUnionStruct struct { + Union MarshallingUnion +} + +func (r *MarshallingUnionStruct) UnmarshalJSON(data []byte) (err error) { + *r = MarshallingUnionStruct{} + err = UnmarshalRoot(data, &r.Union) + return +} + +func (r MarshallingUnionStruct) MarshalJSON() (data []byte, err error) { + return MarshalRoot(r.Union) +} + +type MarshallingUnion interface { + marshallingUnion() +} + +type MarshallingUnionA struct { + Boo string `json:"boo"` +} + +func (MarshallingUnionA) marshallingUnion() {} + +func (r *MarshallingUnionA) UnmarshalJSON(data []byte) (err error) { + return UnmarshalRoot(data, r) +} + +type MarshallingUnionB struct { + Foo string `json:"foo"` +} + +func (MarshallingUnionB) marshallingUnion() {} + +func (r *MarshallingUnionB) UnmarshalJSON(data []byte) (err error) { + return UnmarshalRoot(data, r) +} + +func init() { + RegisterUnion[MarshallingUnion]( + "", + UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(MarshallingUnionA{}), + }, + UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(MarshallingUnionB{}), + }, + ) +} + +var tests = map[string]struct { + buf string + val any +}{ + "true": {"true", true}, + "false": {"false", false}, + "int": {"1", 1}, + "int_bigger": {"12324", 12324}, + "int_string_coerce": {`"65"`, 65}, + "int_boolean_coerce": {"true", 1}, + "int64": {"1", int64(1)}, + "int64_huge": {"123456789123456789", int64(123456789123456789)}, + "uint": {"1", uint(1)}, + "uint_bigger": {"12324", uint(12324)}, + "uint_coerce": {`"65"`, uint(65)}, + "float_1.54": {"1.54", float32(1.54)}, + "float_1.89": {"1.89", float64(1.89)}, + "string": {`"str"`, "str"}, + "string_int_coerce": {`12`, "12"}, + "array_string": {`["foo","bar"]`, []string{"foo", "bar"}}, + "array_int": {`[1,2]`, []int{1, 2}}, + "array_int_coerce": {`["1",2]`, []int{1, 2}}, + + "ptr_true": {"true", P(true)}, + "ptr_false": {"false", P(false)}, + "ptr_int": {"1", P(1)}, + "ptr_int_bigger": {"12324", P(12324)}, + "ptr_int_string_coerce": {`"65"`, P(65)}, + "ptr_int_boolean_coerce": {"true", P(1)}, + "ptr_int64": {"1", P(int64(1))}, + "ptr_int64_huge": {"123456789123456789", P(int64(123456789123456789))}, + "ptr_uint": {"1", P(uint(1))}, + "ptr_uint_bigger": {"12324", P(uint(12324))}, + "ptr_uint_coerce": {`"65"`, P(uint(65))}, + "ptr_float_1.54": {"1.54", P(float32(1.54))}, + "ptr_float_1.89": {"1.89", P(float64(1.89))}, + + "date_time": {`"2007-03-01T13:00:00Z"`, time.Date(2007, time.March, 1, 13, 0, 0, 0, time.UTC)}, + "date_time_nano_coerce": {`"2007-03-01T13:03:05.123456789Z"`, time.Date(2007, time.March, 1, 13, 3, 5, 123456789, time.UTC)}, + + "date_time_missing_t_coerce": {`"2007-03-01 13:03:05Z"`, time.Date(2007, time.March, 1, 13, 3, 5, 0, time.UTC)}, + "date_time_missing_timezone_coerce": {`"2007-03-01T13:03:05"`, time.Date(2007, time.March, 1, 13, 3, 5, 0, time.UTC)}, + // note: using -1200 to minimize probability of conflicting with the local timezone of the test runner + // see https://en.wikipedia.org/wiki/UTC%E2%88%9212:00 + "date_time_missing_timezone_colon_coerce": {`"2007-03-01T13:03:05-1200"`, time.Date(2007, time.March, 1, 13, 3, 5, 0, time.FixedZone("", -12*60*60))}, + "date_time_nano_missing_t_coerce": {`"2007-03-01 13:03:05.123456789Z"`, time.Date(2007, time.March, 1, 13, 3, 5, 123456789, time.UTC)}, + + "map_string": {`{"foo":"bar"}`, map[string]string{"foo": "bar"}}, + "map_string_with_sjson_path_chars": {`{":a.b.c*:d*-1e.f":"bar"}`, map[string]string{":a.b.c*:d*-1e.f": "bar"}}, + "map_interface": {`{"a":1,"b":"str","c":false}`, map[string]any{"a": float64(1), "b": "str", "c": false}}, + + "primitive_struct": { + `{"a":false,"b":237628372683,"c":654,"d":9999.43,"e":43.76,"f":[1,2,3,4]}`, + Primitives{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}, + }, + + "slices": { + `{"slices":[{"a":false,"b":237628372683,"c":654,"d":9999.43,"e":43.76,"f":[1,2,3,4]}]}`, + Slices{ + Slice: []Primitives{{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}}, + }, + }, + + "primitive_pointer_struct": { + `{"a":false,"b":237628372683,"c":654,"d":9999.43,"e":43.76,"f":[1,2,3,4,5]}`, + PrimitivePointers{ + A: P(false), + B: P(237628372683), + C: P(uint(654)), + D: P(9999.43), + E: P(float32(43.76)), + F: &[]int{1, 2, 3, 4, 5}, + }, + }, + + "datetime_struct": { + `{"date":"2006-01-02","date-time":"2006-01-02T15:04:05Z"}`, + DateTime{ + Date: time.Date(2006, time.January, 2, 0, 0, 0, 0, time.UTC), + DateTime: time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC), + }, + }, + + "additional_properties": { + `{"a":true,"bar":"value","foo":true}`, + AdditionalProperties{ + A: true, + ExtraFields: map[string]any{ + "bar": "value", + "foo": true, + }, + }, + }, + + "embedded_struct": { + `{"a":1,"b":"bar"}`, + EmbeddedStructs{ + EmbeddedStruct: EmbeddedStruct{ + A: true, + B: "bar", + JSON: EmbeddedStructJSON{ + A: Field{raw: `1`, status: valid}, + B: Field{raw: `"bar"`, status: valid}, + raw: `{"a":1,"b":"bar"}`, + }, + }, + A: P(1), + ExtraFields: map[string]any{"b": "bar"}, + JSON: EmbeddedStructsJSON{ + A: Field{raw: `1`, status: valid}, + ExtraFields: map[string]Field{ + "b": {raw: `"bar"`, status: valid}, + }, + raw: `{"a":1,"b":"bar"}`, + }, + }, + }, + + "recursive_struct": { + `{"child":{"name":"Alex"},"name":"Robert"}`, + Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}}, + }, + + "metadata_coerce": { + `{"a":"12","b":"12","c":null,"extra_typed":12,"extra_untyped":{"foo":"bar"}}`, + JSONFieldStruct{ + A: false, + B: 12, + C: "", + JSON: JSONFieldStructJSON{ + raw: `{"a":"12","b":"12","c":null,"extra_typed":12,"extra_untyped":{"foo":"bar"}}`, + A: Field{raw: `"12"`, status: invalid}, + B: Field{raw: `"12"`, status: valid}, + C: Field{raw: "null", status: null}, + D: Field{raw: "", status: missing}, + ExtraFields: map[string]Field{ + "extra_typed": { + raw: "12", + status: valid, + }, + "extra_untyped": { + raw: `{"foo":"bar"}`, + status: invalid, + }, + }, + }, + ExtraFields: map[string]int64{ + "extra_typed": 12, + "extra_untyped": 0, + }, + }, + }, + + "unknown_struct_number": { + `{"unknown":12}`, + UnknownStruct{ + Unknown: 12., + }, + }, + + "unknown_struct_map": { + `{"unknown":{"foo":"bar"}}`, + UnknownStruct{ + Unknown: map[string]any{ + "foo": "bar", + }, + }, + }, + + "union_integer": { + `{"union":12}`, + UnionStruct{ + Union: UnionInteger(12), + }, + }, + + "union_struct_discriminated_a": { + `{"union":{"a":"foo","b":"bar","type":"typeA"}}`, + UnionStruct{ + Union: UnionStructA{ + Type: "typeA", + A: "foo", + B: "bar", + }, + }, + }, + + "union_struct_discriminated_b": { + `{"union":{"a":"foo","type":"typeB"}}`, + UnionStruct{ + Union: UnionStructB{ + Type: "typeB", + A: "foo", + }, + }, + }, + + "union_struct_time": { + `{"union":"2010-05-23"}`, + UnionStruct{ + Union: UnionTime(time.Date(2010, 05, 23, 0, 0, 0, 0, time.UTC)), + }, + }, + + "complex_union_a": { + `{"union":{"boo":"12","foo":true}}`, + ComplexUnionStruct{Union: ComplexUnionA{Boo: "12", Foo: true}}, + }, + + "complex_union_b": { + `{"union":{"boo":true,"foo":"12"}}`, + ComplexUnionStruct{Union: ComplexUnionB{Boo: true, Foo: "12"}}, + }, + + "complex_union_c": { + `{"union":{"boo":12}}`, + ComplexUnionStruct{Union: ComplexUnionC{Boo: 12}}, + }, + + "complex_union_type_a": { + `{"union":{"baz":12,"type":"a"}}`, + ComplexUnionStruct{Union: ComplexUnionTypeA{Baz: 12, Type: TypeA("a")}}, + }, + + "complex_union_type_b": { + `{"union":{"baz":12,"type":"b"}}`, + ComplexUnionStruct{Union: ComplexUnionTypeB{Baz: 12, Type: TypeB("b")}}, + }, + + "marshalling_union_a": { + `{"boo":"hello"}`, + MarshallingUnionStruct{Union: MarshallingUnionA{Boo: "hello"}}, + }, + "marshalling_union_b": { + `{"foo":"hi"}`, + MarshallingUnionStruct{Union: MarshallingUnionB{Foo: "hi"}}, + }, + + "unmarshal": { + `{"foo":"hello"}`, + &UnmarshalStruct{Foo: "hello", prop: true}, + }, + + "array_of_unmarshal": { + `[{"foo":"hello"}]`, + []UnmarshalStruct{{Foo: "hello", prop: true}}, + }, + + "inline_coerce": { + `{"a":false,"b":237628372683,"c":654,"d":9999.43,"e":43.76,"f":[1,2,3,4]}`, + Inline{ + InlineField: Primitives{A: false, B: 237628372683, C: 0x28e, D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}, + JSON: InlineJSON{ + InlineField: Field{raw: "{\"a\":false,\"b\":237628372683,\"c\":654,\"d\":9999.43,\"e\":43.76,\"f\":[1,2,3,4]}", status: 3}, + raw: "{\"a\":false,\"b\":237628372683,\"c\":654,\"d\":9999.43,\"e\":43.76,\"f\":[1,2,3,4]}", + }, + }, + }, + + "inline_array_coerce": { + `["Hello","foo","bar"]`, + InlineArray{ + InlineField: []string{"Hello", "foo", "bar"}, + JSON: InlineJSON{ + InlineField: Field{raw: `["Hello","foo","bar"]`, status: 3}, + raw: `["Hello","foo","bar"]`, + }, + }, + }, +} + +func TestDecode(t *testing.T) { + for name, test := range tests { + t.Run(name, func(t *testing.T) { + result := reflect.New(reflect.TypeOf(test.val)) + if err := Unmarshal([]byte(test.buf), result.Interface()); err != nil { + t.Fatalf("deserialization of %v failed with error %v", result, err) + } + if !reflect.DeepEqual(result.Elem().Interface(), test.val) { + t.Fatalf("expected '%s' to deserialize to \n%#v\nbut got\n%#v", test.buf, test.val, result.Elem().Interface()) + } + }) + } +} + +func TestEncode(t *testing.T) { + for name, test := range tests { + if strings.HasSuffix(name, "_coerce") { + continue + } + t.Run(name, func(t *testing.T) { + raw, err := Marshal(test.val) + if err != nil { + t.Fatalf("serialization of %v failed with error %v", test.val, err) + } + if string(raw) != test.buf { + t.Fatalf("expected %+#v to serialize to %s but got %s", test.val, test.buf, string(raw)) + } + }) + } +} diff --git a/internal/apijson/port.go b/internal/apijson/port.go new file mode 100644 index 0000000..b40013c --- /dev/null +++ b/internal/apijson/port.go @@ -0,0 +1,120 @@ +package apijson + +import ( + "fmt" + "reflect" +) + +// Port copies over values from one struct to another struct. +func Port(from any, to any) error { + toVal := reflect.ValueOf(to) + fromVal := reflect.ValueOf(from) + + if toVal.Kind() != reflect.Ptr || toVal.IsNil() { + return fmt.Errorf("destination must be a non-nil pointer") + } + + for toVal.Kind() == reflect.Ptr { + toVal = toVal.Elem() + } + toType := toVal.Type() + + for fromVal.Kind() == reflect.Ptr { + fromVal = fromVal.Elem() + } + fromType := fromVal.Type() + + if toType.Kind() != reflect.Struct { + return fmt.Errorf("destination must be a non-nil pointer to a struct (%v %v)", toType, toType.Kind()) + } + + values := map[string]reflect.Value{} + fields := map[string]reflect.Value{} + + fromJSON := fromVal.FieldByName("JSON") + toJSON := toVal.FieldByName("JSON") + + // Iterate through the fields of v and load all the "normal" fields in the struct to the map of + // string to reflect.Value, as well as their raw .JSON.Foo counterpart indicated by j. + var getFields func(t reflect.Type, v reflect.Value) + getFields = func(t reflect.Type, v reflect.Value) { + j := v.FieldByName("JSON") + + // Recurse into anonymous fields first, since the fields on the object should win over the fields in the + // embedded object. + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + if field.Anonymous { + getFields(field.Type, v.Field(i)) + continue + } + } + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + ptag, ok := parseJSONStructTag(field) + if !ok || ptag.name == "-" || ptag.name == "" { + continue + } + values[ptag.name] = v.Field(i) + if j.IsValid() { + fields[ptag.name] = j.FieldByName(field.Name) + } + } + } + getFields(fromType, fromVal) + + // Use the values from the previous step to populate the 'to' struct. + for i := 0; i < toType.NumField(); i++ { + field := toType.Field(i) + ptag, ok := parseJSONStructTag(field) + if !ok { + continue + } + if ptag.name == "-" { + continue + } + if value, ok := values[ptag.name]; ok { + delete(values, ptag.name) + if field.Type.Kind() == reflect.Interface { + toVal.Field(i).Set(value) + } else { + switch value.Kind() { + case reflect.String: + toVal.Field(i).SetString(value.String()) + case reflect.Bool: + toVal.Field(i).SetBool(value.Bool()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + toVal.Field(i).SetInt(value.Int()) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + toVal.Field(i).SetUint(value.Uint()) + case reflect.Float32, reflect.Float64: + toVal.Field(i).SetFloat(value.Float()) + default: + toVal.Field(i).Set(value) + } + } + } + + if fromJSONField, ok := fields[ptag.name]; ok { + if toJSONField := toJSON.FieldByName(field.Name); toJSONField.IsValid() { + toJSONField.Set(fromJSONField) + } + } + } + + // Finally, copy over the .JSON.raw and .JSON.ExtraFields + if toJSON.IsValid() { + if raw := toJSON.FieldByName("raw"); raw.IsValid() { + setUnexportedField(raw, fromJSON.Interface().(interface{ RawJSON() string }).RawJSON()) + } + + if toExtraFields := toJSON.FieldByName("ExtraFields"); toExtraFields.IsValid() { + if fromExtraFields := fromJSON.FieldByName("ExtraFields"); fromExtraFields.IsValid() { + setUnexportedField(toExtraFields, fromExtraFields.Interface()) + } + } + } + + return nil +} diff --git a/internal/apijson/port_test.go b/internal/apijson/port_test.go new file mode 100644 index 0000000..bb01f1a --- /dev/null +++ b/internal/apijson/port_test.go @@ -0,0 +1,257 @@ +package apijson + +import ( + "reflect" + "testing" +) + +type Metadata struct { + CreatedAt string `json:"created_at"` +} + +// Card is the "combined" type of CardVisa and CardMastercard +type Card struct { + Processor CardProcessor `json:"processor"` + Data any `json:"data"` + IsFoo bool `json:"is_foo"` + IsBar bool `json:"is_bar"` + Metadata Metadata `json:"metadata"` + Value any `json:"value"` + + JSON cardJSON +} + +type cardJSON struct { + Processor Field + Data Field + IsFoo Field + IsBar Field + Metadata Field + Value Field + ExtraFields map[string]Field + raw string +} + +func (r cardJSON) RawJSON() string { return r.raw } + +type CardProcessor string + +// CardVisa +type CardVisa struct { + Processor CardVisaProcessor `json:"processor"` + Data CardVisaData `json:"data"` + IsFoo bool `json:"is_foo"` + Metadata Metadata `json:"metadata"` + Value string `json:"value"` + + JSON cardVisaJSON +} + +type cardVisaJSON struct { + Processor Field + Data Field + IsFoo Field + Metadata Field + Value Field + ExtraFields map[string]Field + raw string +} + +func (r cardVisaJSON) RawJSON() string { return r.raw } + +type CardVisaProcessor string + +type CardVisaData struct { + Foo string `json:"foo"` +} + +// CardMastercard +type CardMastercard struct { + Processor CardMastercardProcessor `json:"processor"` + Data CardMastercardData `json:"data"` + IsBar bool `json:"is_bar"` + Metadata Metadata `json:"metadata"` + Value bool `json:"value"` + + JSON cardMastercardJSON +} + +type cardMastercardJSON struct { + Processor Field + Data Field + IsBar Field + Metadata Field + Value Field + ExtraFields map[string]Field + raw string +} + +func (r cardMastercardJSON) RawJSON() string { return r.raw } + +type CardMastercardProcessor string + +type CardMastercardData struct { + Bar int64 `json:"bar"` +} + +type CommonFields struct { + Metadata Metadata `json:"metadata"` + Value string `json:"value"` + + JSON commonFieldsJSON +} + +type commonFieldsJSON struct { + Metadata Field + Value Field + ExtraFields map[string]Field + raw string +} + +type CardEmbedded struct { + CommonFields + Processor CardVisaProcessor `json:"processor"` + Data CardVisaData `json:"data"` + IsFoo bool `json:"is_foo"` + + JSON cardEmbeddedJSON +} + +type cardEmbeddedJSON struct { + Processor Field + Data Field + IsFoo Field + ExtraFields map[string]Field + raw string +} + +func (r cardEmbeddedJSON) RawJSON() string { return r.raw } + +var portTests = map[string]struct { + from any + to any +}{ + "visa to card": { + CardVisa{ + Processor: "visa", + IsFoo: true, + Data: CardVisaData{ + Foo: "foo", + }, + Metadata: Metadata{ + CreatedAt: "Mar 29 2024", + }, + Value: "value", + JSON: cardVisaJSON{ + raw: `{"processor":"visa","is_foo":true,"data":{"foo":"foo"}}`, + Processor: Field{raw: `"visa"`, status: valid}, + IsFoo: Field{raw: `true`, status: valid}, + Data: Field{raw: `{"foo":"foo"}`, status: valid}, + Value: Field{raw: `"value"`, status: valid}, + ExtraFields: map[string]Field{"extra": {raw: `"yo"`, status: valid}}, + }, + }, + Card{ + Processor: "visa", + IsFoo: true, + IsBar: false, + Data: CardVisaData{ + Foo: "foo", + }, + Metadata: Metadata{ + CreatedAt: "Mar 29 2024", + }, + Value: "value", + JSON: cardJSON{ + raw: `{"processor":"visa","is_foo":true,"data":{"foo":"foo"}}`, + Processor: Field{raw: `"visa"`, status: valid}, + IsFoo: Field{raw: `true`, status: valid}, + Data: Field{raw: `{"foo":"foo"}`, status: valid}, + Value: Field{raw: `"value"`, status: valid}, + ExtraFields: map[string]Field{"extra": {raw: `"yo"`, status: valid}}, + }, + }, + }, + "mastercard to card": { + CardMastercard{ + Processor: "mastercard", + IsBar: true, + Data: CardMastercardData{ + Bar: 13, + }, + Value: false, + }, + Card{ + Processor: "mastercard", + IsFoo: false, + IsBar: true, + Data: CardMastercardData{ + Bar: 13, + }, + Value: false, + }, + }, + "embedded to card": { + CardEmbedded{ + CommonFields: CommonFields{ + Metadata: Metadata{ + CreatedAt: "Mar 29 2024", + }, + Value: "embedded_value", + JSON: commonFieldsJSON{ + Metadata: Field{raw: `{"created_at":"Mar 29 2024"}`, status: valid}, + Value: Field{raw: `"embedded_value"`, status: valid}, + raw: `should not matter`, + }, + }, + Processor: "visa", + IsFoo: true, + Data: CardVisaData{ + Foo: "embedded_foo", + }, + JSON: cardEmbeddedJSON{ + raw: `{"processor":"visa","is_foo":true,"data":{"foo":"embedded_foo"},"metadata":{"created_at":"Mar 29 2024"},"value":"embedded_value"}`, + Processor: Field{raw: `"visa"`, status: valid}, + IsFoo: Field{raw: `true`, status: valid}, + Data: Field{raw: `{"foo":"embedded_foo"}`, status: valid}, + }, + }, + Card{ + Processor: "visa", + IsFoo: true, + IsBar: false, + Data: CardVisaData{ + Foo: "embedded_foo", + }, + Metadata: Metadata{ + CreatedAt: "Mar 29 2024", + }, + Value: "embedded_value", + JSON: cardJSON{ + raw: `{"processor":"visa","is_foo":true,"data":{"foo":"embedded_foo"},"metadata":{"created_at":"Mar 29 2024"},"value":"embedded_value"}`, + Processor: Field{raw: `"visa"`, status: 0x3}, + IsFoo: Field{raw: "true", status: 0x3}, + Data: Field{raw: `{"foo":"embedded_foo"}`, status: 0x3}, + Metadata: Field{raw: `{"created_at":"Mar 29 2024"}`, status: 0x3}, + Value: Field{raw: `"embedded_value"`, status: 0x3}, + }, + }, + }, +} + +func TestPort(t *testing.T) { + for name, test := range portTests { + t.Run(name, func(t *testing.T) { + toVal := reflect.New(reflect.TypeOf(test.to)) + + err := Port(test.from, toVal.Interface()) + if err != nil { + t.Fatalf("port of %v failed with error %v", test.from, err) + } + + if !reflect.DeepEqual(toVal.Elem().Interface(), test.to) { + t.Fatalf("expected:\n%+#v\n\nto port to:\n%+#v\n\nbut got:\n%+#v", test.from, test.to, toVal.Elem().Interface()) + } + }) + } +} diff --git a/internal/apijson/registry.go b/internal/apijson/registry.go new file mode 100644 index 0000000..2a24982 --- /dev/null +++ b/internal/apijson/registry.go @@ -0,0 +1,51 @@ +package apijson + +import ( + "reflect" + + "github.com/tidwall/gjson" +) + +type UnionVariant struct { + TypeFilter gjson.Type + DiscriminatorValue any + Type reflect.Type +} + +var unionRegistry = map[reflect.Type]unionEntry{} +var unionVariants = map[reflect.Type]any{} + +type unionEntry struct { + discriminatorKey string + variants []UnionVariant +} + +func Discriminator[T any](value any) UnionVariant { + var zero T + return UnionVariant{ + TypeFilter: gjson.JSON, + DiscriminatorValue: value, + Type: reflect.TypeOf(zero), + } +} + +func RegisterUnion[T any](discriminator string, variants ...UnionVariant) { + typ := reflect.TypeOf((*T)(nil)).Elem() + unionRegistry[typ] = unionEntry{ + discriminatorKey: discriminator, + variants: variants, + } + for _, variant := range variants { + unionVariants[variant.Type] = typ + } +} + +// Useful to wrap a union type to force it to use [apijson.UnmarshalJSON] since you cannot define an +// UnmarshalJSON function on the interface itself. +type UnionUnmarshaler[T any] struct { + Value T +} + +func (c *UnionUnmarshaler[T]) UnmarshalJSON(buf []byte) error { + return UnmarshalRoot(buf, &c.Value) +} diff --git a/internal/apijson/subfield.go b/internal/apijson/subfield.go new file mode 100644 index 0000000..880277a --- /dev/null +++ b/internal/apijson/subfield.go @@ -0,0 +1,67 @@ +package apijson + +import ( + "github.com/imagekit-developer/imagekit-go/packages/respjson" + "reflect" +) + +func getSubField(root reflect.Value, index []int, name string) reflect.Value { + strct := root.FieldByIndex(index[:len(index)-1]) + if !strct.IsValid() { + panic("couldn't find encapsulating struct for field " + name) + } + meta := strct.FieldByName("JSON") + if !meta.IsValid() { + return reflect.Value{} + } + field := meta.FieldByName(name) + if !field.IsValid() { + return reflect.Value{} + } + return field +} + +func setMetadataSubField(root reflect.Value, index []int, name string, meta Field) { + target := getSubField(root, index, name) + if !target.IsValid() { + return + } + + if target.Type() == reflect.TypeOf(meta) { + target.Set(reflect.ValueOf(meta)) + } else if respMeta := meta.toRespField(); target.Type() == reflect.TypeOf(respMeta) { + target.Set(reflect.ValueOf(respMeta)) + } +} + +func setMetadataExtraFields(root reflect.Value, index []int, name string, metaExtras map[string]Field) { + target := getSubField(root, index, name) + if !target.IsValid() { + return + } + + if target.Type() == reflect.TypeOf(metaExtras) { + target.Set(reflect.ValueOf(metaExtras)) + return + } + + newMap := make(map[string]respjson.Field, len(metaExtras)) + if target.Type() == reflect.TypeOf(newMap) { + for k, v := range metaExtras { + newMap[k] = v.toRespField() + } + target.Set(reflect.ValueOf(newMap)) + } +} + +func (f Field) toRespField() respjson.Field { + if f.IsMissing() { + return respjson.Field{} + } else if f.IsNull() { + return respjson.NewField("null") + } else if f.IsInvalid() { + return respjson.NewInvalidField(f.raw) + } else { + return respjson.NewField(f.raw) + } +} diff --git a/internal/apijson/tag.go b/internal/apijson/tag.go new file mode 100644 index 0000000..812fb3c --- /dev/null +++ b/internal/apijson/tag.go @@ -0,0 +1,47 @@ +package apijson + +import ( + "reflect" + "strings" +) + +const jsonStructTag = "json" +const formatStructTag = "format" + +type parsedStructTag struct { + name string + required bool + extras bool + metadata bool + inline bool +} + +func parseJSONStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) { + raw, ok := field.Tag.Lookup(jsonStructTag) + if !ok { + return + } + parts := strings.Split(raw, ",") + if len(parts) == 0 { + return tag, false + } + tag.name = parts[0] + for _, part := range parts[1:] { + switch part { + case "required": + tag.required = true + case "extras": + tag.extras = true + case "metadata": + tag.metadata = true + case "inline": + tag.inline = true + } + } + return +} + +func parseFormatStructTag(field reflect.StructField) (format string, ok bool) { + format, ok = field.Tag.Lookup(formatStructTag) + return +} diff --git a/internal/apijson/union.go b/internal/apijson/union.go new file mode 100644 index 0000000..9067583 --- /dev/null +++ b/internal/apijson/union.go @@ -0,0 +1,208 @@ +package apijson + +import ( + "errors" + "github.com/imagekit-developer/imagekit-go/packages/param" + "reflect" + + "github.com/tidwall/gjson" +) + +var apiUnionType = reflect.TypeOf(param.APIUnion{}) + +func isStructUnion(t reflect.Type) bool { + if t.Kind() != reflect.Struct { + return false + } + for i := 0; i < t.NumField(); i++ { + if t.Field(i).Type == apiUnionType && t.Field(i).Anonymous { + return true + } + } + return false +} + +func RegisterDiscriminatedUnion[T any](key string, mappings map[string]reflect.Type) { + var t T + entry := unionEntry{ + discriminatorKey: key, + variants: []UnionVariant{}, + } + for k, typ := range mappings { + entry.variants = append(entry.variants, UnionVariant{ + DiscriminatorValue: k, + Type: typ, + }) + } + unionRegistry[reflect.TypeOf(t)] = entry +} + +func (d *decoderBuilder) newStructUnionDecoder(t reflect.Type) decoderFunc { + type variantDecoder struct { + decoder decoderFunc + field reflect.StructField + } + decoders := []variantDecoder{} + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + + if field.Anonymous && field.Type == apiUnionType { + continue + } + + decoder := d.typeDecoder(field.Type) + decoders = append(decoders, variantDecoder{ + decoder: decoder, + field: field, + }) + } + + type discriminatedDecoder struct { + variantDecoder + discriminator any + } + discriminatedDecoders := []discriminatedDecoder{} + unionEntry, discriminated := unionRegistry[t] + for _, variant := range unionEntry.variants { + // For each union variant, find a matching decoder and save it + for _, decoder := range decoders { + if decoder.field.Type.Elem() == variant.Type { + discriminatedDecoders = append(discriminatedDecoders, discriminatedDecoder{ + decoder, + variant.DiscriminatorValue, + }) + break + } + } + } + + return func(n gjson.Result, v reflect.Value, state *decoderState) error { + if discriminated && n.Type == gjson.JSON && len(unionEntry.discriminatorKey) != 0 { + discriminator := n.Get(unionEntry.discriminatorKey).Value() + for _, decoder := range discriminatedDecoders { + if discriminator == decoder.discriminator { + inner := v.FieldByIndex(decoder.field.Index) + return decoder.decoder(n, inner, state) + } + } + return errors.New("apijson: was not able to find discriminated union variant") + } + + // Set bestExactness to worse than loose + bestExactness := loose - 1 + bestVariant := -1 + for i, decoder := range decoders { + // Pointers are used to discern JSON object variants from value variants + if n.Type != gjson.JSON && decoder.field.Type.Kind() == reflect.Ptr { + continue + } + + sub := decoderState{strict: state.strict, exactness: exact} + inner := v.FieldByIndex(decoder.field.Index) + err := decoder.decoder(n, inner, &sub) + if err != nil { + continue + } + if sub.exactness == exact { + bestExactness = exact + bestVariant = i + break + } + if sub.exactness > bestExactness { + bestExactness = sub.exactness + bestVariant = i + } + } + + if bestExactness < loose { + return errors.New("apijson: was not able to coerce type as union") + } + + if guardStrict(state, bestExactness != exact) { + return errors.New("apijson: was not able to coerce type as union strictly") + } + + for i := 0; i < len(decoders); i++ { + if i == bestVariant { + continue + } + v.FieldByIndex(decoders[i].field.Index).SetZero() + } + + return nil + } +} + +// newUnionDecoder returns a decoderFunc that deserializes into a union using an +// algorithm roughly similar to Pydantic's [smart algorithm]. +// +// Conceptually this is equivalent to choosing the best schema based on how 'exact' +// the deserialization is for each of the schemas. +// +// If there is a tie in the level of exactness, then the tie is broken +// left-to-right. +// +// [smart algorithm]: https://docs.pydantic.dev/latest/concepts/unions/#smart-mode +func (d *decoderBuilder) newUnionDecoder(t reflect.Type) decoderFunc { + unionEntry, ok := unionRegistry[t] + if !ok { + panic("apijson: couldn't find union of type " + t.String() + " in union registry") + } + decoders := []decoderFunc{} + for _, variant := range unionEntry.variants { + decoder := d.typeDecoder(variant.Type) + decoders = append(decoders, decoder) + } + return func(n gjson.Result, v reflect.Value, state *decoderState) error { + // If there is a discriminator match, circumvent the exactness logic entirely + for idx, variant := range unionEntry.variants { + decoder := decoders[idx] + if variant.TypeFilter != n.Type { + continue + } + + if len(unionEntry.discriminatorKey) != 0 { + discriminatorValue := n.Get(unionEntry.discriminatorKey).Value() + if discriminatorValue == variant.DiscriminatorValue { + inner := reflect.New(variant.Type).Elem() + err := decoder(n, inner, state) + v.Set(inner) + return err + } + } + } + + // Set bestExactness to worse than loose + bestExactness := loose - 1 + for idx, variant := range unionEntry.variants { + decoder := decoders[idx] + if variant.TypeFilter != n.Type { + continue + } + sub := decoderState{strict: state.strict, exactness: exact} + inner := reflect.New(variant.Type).Elem() + err := decoder(n, inner, &sub) + if err != nil { + continue + } + if sub.exactness == exact { + v.Set(inner) + return nil + } + if sub.exactness > bestExactness { + v.Set(inner) + bestExactness = sub.exactness + } + } + + if bestExactness < loose { + return errors.New("apijson: was not able to coerce type as union") + } + + if guardStrict(state, bestExactness != exact) { + return errors.New("apijson: was not able to coerce type as union strictly") + } + + return nil + } +} diff --git a/internal/apiquery/encoder.go b/internal/apiquery/encoder.go new file mode 100644 index 0000000..e4293f4 --- /dev/null +++ b/internal/apiquery/encoder.go @@ -0,0 +1,415 @@ +package apiquery + +import ( + "encoding/json" + "fmt" + "reflect" + "strconv" + "strings" + "sync" + "time" + + "github.com/imagekit-developer/imagekit-go/packages/param" +) + +var encoders sync.Map // map[reflect.Type]encoderFunc + +type encoder struct { + dateFormat string + root bool + settings QuerySettings +} + +type encoderFunc func(key string, value reflect.Value) ([]Pair, error) + +type encoderField struct { + tag parsedStructTag + fn encoderFunc + idx []int +} + +type encoderEntry struct { + reflect.Type + dateFormat string + root bool + settings QuerySettings +} + +type Pair struct { + key string + value string +} + +func (e *encoder) typeEncoder(t reflect.Type) encoderFunc { + entry := encoderEntry{ + Type: t, + dateFormat: e.dateFormat, + root: e.root, + settings: e.settings, + } + + if fi, ok := encoders.Load(entry); ok { + return fi.(encoderFunc) + } + + // To deal with recursive types, populate the map with an + // indirect func before we build it. This type waits on the + // real func (f) to be ready and then calls it. This indirect + // func is only used for recursive types. + var ( + wg sync.WaitGroup + f encoderFunc + ) + wg.Add(1) + fi, loaded := encoders.LoadOrStore(entry, encoderFunc(func(key string, v reflect.Value) ([]Pair, error) { + wg.Wait() + return f(key, v) + })) + if loaded { + return fi.(encoderFunc) + } + + // Compute the real encoder and replace the indirect func with it. + f = e.newTypeEncoder(t) + wg.Done() + encoders.Store(entry, f) + return f +} + +func marshalerEncoder(key string, value reflect.Value) ([]Pair, error) { + s, err := value.Interface().(json.Marshaler).MarshalJSON() + if err != nil { + return nil, fmt.Errorf("apiquery: json fallback marshal error %s", err) + } + return []Pair{{key, string(s)}}, nil +} + +func (e *encoder) newTypeEncoder(t reflect.Type) encoderFunc { + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + return e.newTimeTypeEncoder(t) + } + + if t.Implements(reflect.TypeOf((*param.Optional)(nil)).Elem()) { + return e.newRichFieldTypeEncoder(t) + } + + if !e.root && t.Implements(reflect.TypeOf((*json.Marshaler)(nil)).Elem()) { + return marshalerEncoder + } + + e.root = false + switch t.Kind() { + case reflect.Pointer: + encoder := e.typeEncoder(t.Elem()) + return func(key string, value reflect.Value) (pairs []Pair, err error) { + if !value.IsValid() || value.IsNil() { + return + } + return encoder(key, value.Elem()) + } + case reflect.Struct: + return e.newStructTypeEncoder(t) + case reflect.Array: + fallthrough + case reflect.Slice: + return e.newArrayTypeEncoder(t) + case reflect.Map: + return e.newMapEncoder(t) + case reflect.Interface: + return e.newInterfaceEncoder() + default: + return e.newPrimitiveTypeEncoder(t) + } +} + +func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { + if t.Implements(reflect.TypeOf((*param.Optional)(nil)).Elem()) { + return e.newRichFieldTypeEncoder(t) + } + + for i := 0; i < t.NumField(); i++ { + if t.Field(i).Type == paramUnionType && t.Field(i).Anonymous { + return e.newStructUnionTypeEncoder(t) + } + } + + encoderFields := []encoderField{} + + // This helper allows us to recursively collect field encoders into a flat + // array. The parameter `index` keeps track of the access patterns necessary + // to get to some field. + var collectEncoderFields func(r reflect.Type, index []int) + collectEncoderFields = func(r reflect.Type, index []int) { + for i := 0; i < r.NumField(); i++ { + idx := append(index, i) + field := t.FieldByIndex(idx) + if !field.IsExported() { + continue + } + // If this is an embedded struct, traverse one level deeper to extract + // the field and get their encoders as well. + if field.Anonymous { + collectEncoderFields(field.Type, idx) + continue + } + // If query tag is not present, then we skip, which is intentionally + // different behavior from the stdlib. + ptag, ok := parseQueryStructTag(field) + if !ok { + continue + } + + if (ptag.name == "-" || ptag.name == "") && !ptag.inline { + continue + } + + dateFormat, ok := parseFormatStructTag(field) + oldFormat := e.dateFormat + if ok { + switch dateFormat { + case "date-time": + e.dateFormat = time.RFC3339 + case "date": + e.dateFormat = "2006-01-02" + } + } + var encoderFn encoderFunc + if ptag.omitzero { + typeEncoderFn := e.typeEncoder(field.Type) + encoderFn = func(key string, value reflect.Value) ([]Pair, error) { + if value.IsZero() { + return nil, nil + } + return typeEncoderFn(key, value) + } + } else { + encoderFn = e.typeEncoder(field.Type) + } + encoderFields = append(encoderFields, encoderField{ptag, encoderFn, idx}) + e.dateFormat = oldFormat + } + } + collectEncoderFields(t, []int{}) + + return func(key string, value reflect.Value) (pairs []Pair, err error) { + for _, ef := range encoderFields { + var subkey string = e.renderKeyPath(key, ef.tag.name) + if ef.tag.inline { + subkey = key + } + + field := value.FieldByIndex(ef.idx) + subpairs, suberr := ef.fn(subkey, field) + if suberr != nil { + err = suberr + } + pairs = append(pairs, subpairs...) + } + return + } +} + +var paramUnionType = reflect.TypeOf((*param.APIUnion)(nil)).Elem() + +func (e *encoder) newStructUnionTypeEncoder(t reflect.Type) encoderFunc { + var fieldEncoders []encoderFunc + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + if field.Type == paramUnionType && field.Anonymous { + fieldEncoders = append(fieldEncoders, nil) + continue + } + fieldEncoders = append(fieldEncoders, e.typeEncoder(field.Type)) + } + + return func(key string, value reflect.Value) (pairs []Pair, err error) { + for i := 0; i < t.NumField(); i++ { + if value.Field(i).Type() == paramUnionType { + continue + } + if !value.Field(i).IsZero() { + return fieldEncoders[i](key, value.Field(i)) + } + } + return nil, fmt.Errorf("apiquery: union %s has no field set", t.String()) + } +} + +func (e *encoder) newMapEncoder(t reflect.Type) encoderFunc { + keyEncoder := e.typeEncoder(t.Key()) + elementEncoder := e.typeEncoder(t.Elem()) + return func(key string, value reflect.Value) (pairs []Pair, err error) { + iter := value.MapRange() + for iter.Next() { + encodedKey, err := keyEncoder("", iter.Key()) + if err != nil { + return nil, err + } + if len(encodedKey) != 1 { + return nil, fmt.Errorf("apiquery: unexpected number of parts for encoded map key, map may contain non-primitive") + } + subkey := encodedKey[0].value + keyPath := e.renderKeyPath(key, subkey) + subpairs, suberr := elementEncoder(keyPath, iter.Value()) + if suberr != nil { + err = suberr + } + pairs = append(pairs, subpairs...) + } + return + } +} + +func (e *encoder) renderKeyPath(key string, subkey string) string { + if len(key) == 0 { + return subkey + } + if e.settings.NestedFormat == NestedQueryFormatDots { + return fmt.Sprintf("%s.%s", key, subkey) + } + return fmt.Sprintf("%s[%s]", key, subkey) +} + +func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc { + switch e.settings.ArrayFormat { + case ArrayQueryFormatComma: + innerEncoder := e.typeEncoder(t.Elem()) + return func(key string, v reflect.Value) ([]Pair, error) { + elements := []string{} + for i := 0; i < v.Len(); i++ { + innerPairs, err := innerEncoder("", v.Index(i)) + if err != nil { + return nil, err + } + for _, pair := range innerPairs { + elements = append(elements, pair.value) + } + } + if len(elements) == 0 { + return []Pair{}, nil + } + return []Pair{{key, strings.Join(elements, ",")}}, nil + } + case ArrayQueryFormatRepeat: + innerEncoder := e.typeEncoder(t.Elem()) + return func(key string, value reflect.Value) (pairs []Pair, err error) { + for i := 0; i < value.Len(); i++ { + subpairs, suberr := innerEncoder(key, value.Index(i)) + if suberr != nil { + err = suberr + } + pairs = append(pairs, subpairs...) + } + return + } + case ArrayQueryFormatIndices: + panic("The array indices format is not supported yet") + case ArrayQueryFormatBrackets: + innerEncoder := e.typeEncoder(t.Elem()) + return func(key string, value reflect.Value) (pairs []Pair, err error) { + pairs = []Pair{} + for i := 0; i < value.Len(); i++ { + subpairs, suberr := innerEncoder(key+"[]", value.Index(i)) + if suberr != nil { + err = suberr + } + pairs = append(pairs, subpairs...) + } + return + } + default: + panic(fmt.Sprintf("Unknown ArrayFormat value: %d", e.settings.ArrayFormat)) + } +} + +func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc { + switch t.Kind() { + case reflect.Pointer: + inner := t.Elem() + + innerEncoder := e.newPrimitiveTypeEncoder(inner) + return func(key string, v reflect.Value) ([]Pair, error) { + if !v.IsValid() || v.IsNil() { + return nil, nil + } + return innerEncoder(key, v.Elem()) + } + case reflect.String: + return func(key string, v reflect.Value) ([]Pair, error) { + return []Pair{{key, v.String()}}, nil + } + case reflect.Bool: + return func(key string, v reflect.Value) ([]Pair, error) { + if v.Bool() { + return []Pair{{key, "true"}}, nil + } + return []Pair{{key, "false"}}, nil + } + case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64: + return func(key string, v reflect.Value) ([]Pair, error) { + return []Pair{{key, strconv.FormatInt(v.Int(), 10)}}, nil + } + case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return func(key string, v reflect.Value) ([]Pair, error) { + return []Pair{{key, strconv.FormatUint(v.Uint(), 10)}}, nil + } + case reflect.Float32, reflect.Float64: + return func(key string, v reflect.Value) ([]Pair, error) { + return []Pair{{key, strconv.FormatFloat(v.Float(), 'f', -1, 64)}}, nil + } + case reflect.Complex64, reflect.Complex128: + bitSize := 64 + if t.Kind() == reflect.Complex128 { + bitSize = 128 + } + return func(key string, v reflect.Value) ([]Pair, error) { + return []Pair{{key, strconv.FormatComplex(v.Complex(), 'f', -1, bitSize)}}, nil + } + default: + return func(key string, v reflect.Value) ([]Pair, error) { + return nil, nil + } + } +} + +func (e *encoder) newFieldTypeEncoder(t reflect.Type) encoderFunc { + f, _ := t.FieldByName("Value") + enc := e.typeEncoder(f.Type) + + return func(key string, value reflect.Value) ([]Pair, error) { + present := value.FieldByName("Present") + if !present.Bool() { + return nil, nil + } + null := value.FieldByName("Null") + if null.Bool() { + return nil, fmt.Errorf("apiquery: field cannot be null") + } + raw := value.FieldByName("Raw") + if !raw.IsNil() { + return e.typeEncoder(raw.Type())(key, raw) + } + return enc(key, value.FieldByName("Value")) + } +} + +func (e *encoder) newTimeTypeEncoder(_ reflect.Type) encoderFunc { + format := e.dateFormat + return func(key string, value reflect.Value) ([]Pair, error) { + return []Pair{{ + key, + value.Convert(reflect.TypeOf(time.Time{})).Interface().(time.Time).Format(format), + }}, nil + } +} + +func (e encoder) newInterfaceEncoder() encoderFunc { + return func(key string, value reflect.Value) ([]Pair, error) { + value = value.Elem() + if !value.IsValid() { + return nil, nil + } + return e.typeEncoder(value.Type())(key, value) + } + +} diff --git a/internal/apiquery/query.go b/internal/apiquery/query.go new file mode 100644 index 0000000..0f379fa --- /dev/null +++ b/internal/apiquery/query.go @@ -0,0 +1,55 @@ +package apiquery + +import ( + "net/url" + "reflect" + "time" +) + +func MarshalWithSettings(value any, settings QuerySettings) (url.Values, error) { + e := encoder{time.RFC3339, true, settings} + kv := url.Values{} + val := reflect.ValueOf(value) + if !val.IsValid() { + return nil, nil + } + typ := val.Type() + + pairs, err := e.typeEncoder(typ)("", val) + if err != nil { + return nil, err + } + for _, pair := range pairs { + kv.Add(pair.key, pair.value) + } + return kv, nil +} + +func Marshal(value any) (url.Values, error) { + return MarshalWithSettings(value, QuerySettings{}) +} + +type Queryer interface { + URLQuery() (url.Values, error) +} + +type QuerySettings struct { + NestedFormat NestedQueryFormat + ArrayFormat ArrayQueryFormat +} + +type NestedQueryFormat int + +const ( + NestedQueryFormatBrackets NestedQueryFormat = iota + NestedQueryFormatDots +) + +type ArrayQueryFormat int + +const ( + ArrayQueryFormatComma ArrayQueryFormat = iota + ArrayQueryFormatRepeat + ArrayQueryFormatIndices + ArrayQueryFormatBrackets +) diff --git a/internal/apiquery/query_test.go b/internal/apiquery/query_test.go new file mode 100644 index 0000000..5b1c33c --- /dev/null +++ b/internal/apiquery/query_test.go @@ -0,0 +1,435 @@ +package apiquery + +import ( + "github.com/imagekit-developer/imagekit-go/packages/param" + "net/url" + "testing" + "time" +) + +func P[T any](v T) *T { return &v } + +type Primitives struct { + A bool `query:"a"` + B int `query:"b"` + C uint `query:"c"` + D float64 `query:"d"` + E float32 `query:"e"` + F []int `query:"f"` +} + +type PrimitivePointers struct { + A *bool `query:"a"` + B *int `query:"b"` + C *uint `query:"c"` + D *float64 `query:"d"` + E *float32 `query:"e"` + F *[]int `query:"f"` +} + +type Slices struct { + Slice []Primitives `query:"slices"` + Mixed []any `query:"mixed"` +} + +type DateTime struct { + Date time.Time `query:"date" format:"date"` + DateTime time.Time `query:"date-time" format:"date-time"` +} + +type AdditionalProperties struct { + A bool `query:"a"` + Extras map[string]any `query:"-,inline"` +} + +type Recursive struct { + Name string `query:"name"` + Child *Recursive `query:"child"` +} + +type UnknownStruct struct { + Unknown any `query:"unknown"` +} + +type UnionStruct struct { + Union Union `query:"union" format:"date"` +} + +type Union interface { + union() +} + +type UnionInteger int64 + +func (UnionInteger) union() {} + +type UnionString string + +func (UnionString) union() {} + +type UnionStructA struct { + Type string `query:"type"` + A string `query:"a"` + B string `query:"b"` +} + +func (UnionStructA) union() {} + +type UnionStructB struct { + Type string `query:"type"` + A string `query:"a"` +} + +func (UnionStructB) union() {} + +type UnionTime time.Time + +func (UnionTime) union() {} + +type DeeplyNested struct { + A DeeplyNested1 `query:"a"` +} + +type DeeplyNested1 struct { + B DeeplyNested2 `query:"b"` +} + +type DeeplyNested2 struct { + C DeeplyNested3 `query:"c"` +} + +type DeeplyNested3 struct { + D *string `query:"d"` +} + +type RichPrimitives struct { + A param.Opt[string] `query:"a"` +} + +type QueryOmitTest struct { + A param.Opt[string] `query:"a,omitzero"` + B string `query:"b,omitzero"` +} + +type NamedEnum string + +const NamedEnumFoo NamedEnum = "foo" + +type StructUnionWrapper struct { + Union StructUnion `query:"union"` +} + +type StructUnion struct { + OfInt param.Opt[int64] `query:",omitzero,inline"` + OfString param.Opt[string] `query:",omitzero,inline"` + OfEnum param.Opt[NamedEnum] `query:",omitzero,inline"` + OfA UnionStructA `query:",omitzero,inline"` + OfB UnionStructB `query:",omitzero,inline"` + param.APIUnion +} + +var tests = map[string]struct { + enc string + val any + settings QuerySettings +}{ + "primitives": { + "a=false&b=237628372683&c=654&d=9999.43&e=43.7599983215332&f=1,2,3,4", + Primitives{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}, + QuerySettings{}, + }, + + "slices_brackets": { + `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`, + Slices{ + Slice: []Primitives{ + {A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}, + {A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}, + }, + Mixed: []any{1, 2.3, "hello"}, + }, + QuerySettings{ArrayFormat: ArrayQueryFormatBrackets}, + }, + + "slices_comma": { + `mixed=1,2.3,hello`, + Slices{ + Mixed: []any{1, 2.3, "hello"}, + }, + QuerySettings{ArrayFormat: ArrayQueryFormatComma}, + }, + + "slices_repeat": { + `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`, + Slices{ + Slice: []Primitives{ + {A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}, + {A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}, + }, + Mixed: []any{1, 2.3, "hello"}, + }, + QuerySettings{ArrayFormat: ArrayQueryFormatRepeat}, + }, + + "primitive_pointer_struct": { + "a=false&b=237628372683&c=654&d=9999.43&e=43.7599983215332&f=1,2,3,4,5", + PrimitivePointers{ + A: P(false), + B: P(237628372683), + C: P(uint(654)), + D: P(9999.43), + E: P(float32(43.76)), + F: &[]int{1, 2, 3, 4, 5}, + }, + QuerySettings{}, + }, + + "datetime_struct": { + `date=2006-01-02&date-time=2006-01-02T15:04:05Z`, + DateTime{ + Date: time.Date(2006, time.January, 2, 0, 0, 0, 0, time.UTC), + DateTime: time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC), + }, + QuerySettings{}, + }, + + "additional_properties": { + `a=true&bar=value&foo=true`, + AdditionalProperties{ + A: true, + Extras: map[string]any{ + "bar": "value", + "foo": true, + }, + }, + QuerySettings{}, + }, + + "recursive_struct_brackets": { + `child[name]=Alex&name=Robert`, + Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}}, + QuerySettings{NestedFormat: NestedQueryFormatBrackets}, + }, + + "recursive_struct_dots": { + `child.name=Alex&name=Robert`, + Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}}, + QuerySettings{NestedFormat: NestedQueryFormatDots}, + }, + + "unknown_struct_number": { + `unknown=12`, + UnknownStruct{ + Unknown: 12., + }, + QuerySettings{}, + }, + + "unknown_struct_map_brackets": { + `unknown[foo]=bar`, + UnknownStruct{ + Unknown: map[string]any{ + "foo": "bar", + }, + }, + QuerySettings{NestedFormat: NestedQueryFormatBrackets}, + }, + + "unknown_struct_map_dots": { + `unknown.foo=bar`, + UnknownStruct{ + Unknown: map[string]any{ + "foo": "bar", + }, + }, + QuerySettings{NestedFormat: NestedQueryFormatDots}, + }, + + "struct_union_string": { + `union=hello`, + StructUnionWrapper{ + Union: StructUnion{OfString: param.NewOpt("hello")}, + }, + QuerySettings{}, + }, + + "union_string": { + `union=hello`, + UnionStruct{ + Union: UnionString("hello"), + }, + QuerySettings{}, + }, + + "struct_union_integer": { + `union=12`, + StructUnionWrapper{ + Union: StructUnion{OfInt: param.NewOpt[int64](12)}, + }, + QuerySettings{}, + }, + + "union_integer": { + `union=12`, + UnionStruct{ + Union: UnionInteger(12), + }, + QuerySettings{}, + }, + + "struct_union_enum": { + `union=foo`, + StructUnionWrapper{ + Union: StructUnion{OfEnum: param.NewOpt[NamedEnum](NamedEnumFoo)}, + }, + QuerySettings{}, + }, + + "struct_union_struct_discriminated_a": { + `union[a]=foo&union[b]=bar&union[type]=typeA`, + StructUnionWrapper{ + Union: StructUnion{OfA: UnionStructA{ + Type: "typeA", + A: "foo", + B: "bar", + }}, + }, + QuerySettings{}, + }, + + "union_struct_discriminated_a": { + `union[a]=foo&union[b]=bar&union[type]=typeA`, + UnionStruct{ + Union: UnionStructA{ + Type: "typeA", + A: "foo", + B: "bar", + }, + }, + QuerySettings{}, + }, + + "struct_union_struct_discriminated_b": { + `union[a]=foo&union[type]=typeB`, + StructUnionWrapper{ + Union: StructUnion{OfB: UnionStructB{ + Type: "typeB", + A: "foo", + }}, + }, + QuerySettings{}, + }, + + "union_struct_discriminated_b": { + `union[a]=foo&union[type]=typeB`, + UnionStruct{ + Union: UnionStructB{ + Type: "typeB", + A: "foo", + }, + }, + QuerySettings{}, + }, + + "union_struct_time": { + `union=2010-05-23`, + UnionStruct{ + Union: UnionTime(time.Date(2010, 05, 23, 0, 0, 0, 0, time.UTC)), + }, + QuerySettings{}, + }, + + "deeply_nested_brackets": { + `a[b][c][d]=hello`, + DeeplyNested{ + A: DeeplyNested1{ + B: DeeplyNested2{ + C: DeeplyNested3{ + D: P("hello"), + }, + }, + }, + }, + QuerySettings{NestedFormat: NestedQueryFormatBrackets}, + }, + + "deeply_nested_dots": { + `a.b.c.d=hello`, + DeeplyNested{ + A: DeeplyNested1{ + B: DeeplyNested2{ + C: DeeplyNested3{ + D: P("hello"), + }, + }, + }, + }, + QuerySettings{NestedFormat: NestedQueryFormatDots}, + }, + + "deeply_nested_brackets_empty": { + ``, + DeeplyNested{ + A: DeeplyNested1{ + B: DeeplyNested2{ + C: DeeplyNested3{ + D: nil, + }, + }, + }, + }, + QuerySettings{NestedFormat: NestedQueryFormatBrackets}, + }, + + "deeply_nested_dots_empty": { + ``, + DeeplyNested{ + A: DeeplyNested1{ + B: DeeplyNested2{ + C: DeeplyNested3{ + D: nil, + }, + }, + }, + }, + QuerySettings{NestedFormat: NestedQueryFormatDots}, + }, + + "rich_primitives": { + `a=hello`, + RichPrimitives{ + A: param.Opt[string]{Value: "hello"}, + }, + QuerySettings{}, + }, + + "rich_primitives_omit": { + ``, + QueryOmitTest{ + A: param.Opt[string]{}, + }, + QuerySettings{}, + }, + "query_omit": { + `a=hello`, + QueryOmitTest{ + A: param.Opt[string]{Value: "hello"}, + }, + QuerySettings{}, + }, +} + +func TestEncode(t *testing.T) { + for name, test := range tests { + t.Run(name, func(t *testing.T) { + values, err := MarshalWithSettings(test.val, test.settings) + if err != nil { + t.Fatalf("failed to marshal url %s", err) + } + str, _ := url.QueryUnescape(values.Encode()) + if str != test.enc { + t.Fatalf("expected %+#v to serialize to %s but got %s", test.val, test.enc, str) + } + }) + } +} diff --git a/internal/apiquery/richparam.go b/internal/apiquery/richparam.go new file mode 100644 index 0000000..61231e1 --- /dev/null +++ b/internal/apiquery/richparam.go @@ -0,0 +1,19 @@ +package apiquery + +import ( + "github.com/imagekit-developer/imagekit-go/packages/param" + "reflect" +) + +func (e *encoder) newRichFieldTypeEncoder(t reflect.Type) encoderFunc { + f, _ := t.FieldByName("Value") + enc := e.typeEncoder(f.Type) + return func(key string, value reflect.Value) ([]Pair, error) { + if opt, ok := value.Interface().(param.Optional); ok && opt.Valid() { + return enc(key, value.FieldByIndex(f.Index)) + } else if ok && param.IsNull(opt) { + return []Pair{{key, "null"}}, nil + } + return nil, nil + } +} diff --git a/internal/apiquery/tag.go b/internal/apiquery/tag.go new file mode 100644 index 0000000..772c40e --- /dev/null +++ b/internal/apiquery/tag.go @@ -0,0 +1,44 @@ +package apiquery + +import ( + "reflect" + "strings" +) + +const queryStructTag = "query" +const formatStructTag = "format" + +type parsedStructTag struct { + name string + omitempty bool + omitzero bool + inline bool +} + +func parseQueryStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) { + raw, ok := field.Tag.Lookup(queryStructTag) + if !ok { + return + } + parts := strings.Split(raw, ",") + if len(parts) == 0 { + return tag, false + } + tag.name = parts[0] + for _, part := range parts[1:] { + switch part { + case "omitzero": + tag.omitzero = true + case "omitempty": + tag.omitempty = true + case "inline": + tag.inline = true + } + } + return +} + +func parseFormatStructTag(field reflect.StructField) (format string, ok bool) { + format, ok = field.Tag.Lookup(formatStructTag) + return +} diff --git a/internal/encoding/json/decode.go b/internal/encoding/json/decode.go new file mode 100644 index 0000000..130edbf --- /dev/null +++ b/internal/encoding/json/decode.go @@ -0,0 +1,1324 @@ +// Vendored from Go 1.24.0-pre-release +// To find alterations, check package shims, and comments beginning in SHIM(). +// +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Represents JSON data structure using native Go types: booleans, floats, +// strings, arrays, and maps. + +package json + +import ( + "encoding" + "encoding/base64" + "fmt" + "github.com/imagekit-developer/imagekit-go/internal/encoding/json/shims" + "reflect" + "strconv" + "strings" + "unicode" + "unicode/utf16" + "unicode/utf8" + _ "unsafe" // for linkname +) + +// Unmarshal parses the JSON-encoded data and stores the result +// in the value pointed to by v. If v is nil or not a pointer, +// Unmarshal returns an [InvalidUnmarshalError]. +// +// Unmarshal uses the inverse of the encodings that +// [Marshal] uses, allocating maps, slices, and pointers as necessary, +// with the following additional rules: +// +// To unmarshal JSON into a pointer, Unmarshal first handles the case of +// the JSON being the JSON literal null. In that case, Unmarshal sets +// the pointer to nil. Otherwise, Unmarshal unmarshals the JSON into +// the value pointed at by the pointer. If the pointer is nil, Unmarshal +// allocates a new value for it to point to. +// +// To unmarshal JSON into a value implementing [Unmarshaler], +// Unmarshal calls that value's [Unmarshaler.UnmarshalJSON] method, including +// when the input is a JSON null. +// Otherwise, if the value implements [encoding.TextUnmarshaler] +// and the input is a JSON quoted string, Unmarshal calls +// [encoding.TextUnmarshaler.UnmarshalText] with the unquoted form of the string. +// +// To unmarshal JSON into a struct, Unmarshal matches incoming object +// keys to the keys used by [Marshal] (either the struct field name or its tag), +// preferring an exact match but also accepting a case-insensitive match. By +// default, object keys which don't have a corresponding struct field are +// ignored (see [Decoder.DisallowUnknownFields] for an alternative). +// +// To unmarshal JSON into an interface value, +// Unmarshal stores one of these in the interface value: +// +// - bool, for JSON booleans +// - float64, for JSON numbers +// - string, for JSON strings +// - []any, for JSON arrays +// - map[string]any, for JSON objects +// - nil for JSON null +// +// To unmarshal a JSON array into a slice, Unmarshal resets the slice length +// to zero and then appends each element to the slice. +// As a special case, to unmarshal an empty JSON array into a slice, +// Unmarshal replaces the slice with a new empty slice. +// +// To unmarshal a JSON array into a Go array, Unmarshal decodes +// JSON array elements into corresponding Go array elements. +// If the Go array is smaller than the JSON array, +// the additional JSON array elements are discarded. +// If the JSON array is smaller than the Go array, +// the additional Go array elements are set to zero values. +// +// To unmarshal a JSON object into a map, Unmarshal first establishes a map to +// use. If the map is nil, Unmarshal allocates a new map. Otherwise Unmarshal +// reuses the existing map, keeping existing entries. Unmarshal then stores +// key-value pairs from the JSON object into the map. The map's key type must +// either be any string type, an integer, or implement [encoding.TextUnmarshaler]. +// +// If the JSON-encoded data contain a syntax error, Unmarshal returns a [SyntaxError]. +// +// If a JSON value is not appropriate for a given target type, +// or if a JSON number overflows the target type, Unmarshal +// skips that field and completes the unmarshaling as best it can. +// If no more serious errors are encountered, Unmarshal returns +// an [UnmarshalTypeError] describing the earliest such error. In any +// case, it's not guaranteed that all the remaining fields following +// the problematic one will be unmarshaled into the target object. +// +// The JSON null value unmarshals into an interface, map, pointer, or slice +// by setting that Go value to nil. Because null is often used in JSON to mean +// “not present,” unmarshaling a JSON null into any other Go type has no effect +// on the value and produces no error. +// +// When unmarshaling quoted strings, invalid UTF-8 or +// invalid UTF-16 surrogate pairs are not treated as an error. +// Instead, they are replaced by the Unicode replacement +// character U+FFFD. +func Unmarshal(data []byte, v any) error { + // Check for well-formedness. + // Avoids filling out half a data structure + // before discovering a JSON syntax error. + var d decodeState + err := checkValid(data, &d.scan) + if err != nil { + return err + } + + d.init(data) + return d.unmarshal(v) +} + +// Unmarshaler is the interface implemented by types +// that can unmarshal a JSON description of themselves. +// The input can be assumed to be a valid encoding of +// a JSON value. UnmarshalJSON must copy the JSON data +// if it wishes to retain the data after returning. +// +// By convention, to approximate the behavior of [Unmarshal] itself, +// Unmarshalers implement UnmarshalJSON([]byte("null")) as a no-op. +type Unmarshaler interface { + UnmarshalJSON([]byte) error +} + +// An UnmarshalTypeError describes a JSON value that was +// not appropriate for a value of a specific Go type. +type UnmarshalTypeError struct { + Value string // description of JSON value - "bool", "array", "number -5" + Type reflect.Type // type of Go value it could not be assigned to + Offset int64 // error occurred after reading Offset bytes + Struct string // name of the struct type containing the field + Field string // the full path from root node to the field, include embedded struct +} + +func (e *UnmarshalTypeError) Error() string { + if e.Struct != "" || e.Field != "" { + return "json: cannot unmarshal " + e.Value + " into Go struct field " + e.Struct + "." + e.Field + " of type " + e.Type.String() + } + return "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String() +} + +// An UnmarshalFieldError describes a JSON object key that +// led to an unexported (and therefore unwritable) struct field. +// +// Deprecated: No longer used; kept for compatibility. +type UnmarshalFieldError struct { + Key string + Type reflect.Type + Field reflect.StructField +} + +func (e *UnmarshalFieldError) Error() string { + return "json: cannot unmarshal object key " + strconv.Quote(e.Key) + " into unexported field " + e.Field.Name + " of type " + e.Type.String() +} + +// An InvalidUnmarshalError describes an invalid argument passed to [Unmarshal]. +// (The argument to [Unmarshal] must be a non-nil pointer.) +type InvalidUnmarshalError struct { + Type reflect.Type +} + +func (e *InvalidUnmarshalError) Error() string { + if e.Type == nil { + return "json: Unmarshal(nil)" + } + + if e.Type.Kind() != reflect.Pointer { + return "json: Unmarshal(non-pointer " + e.Type.String() + ")" + } + return "json: Unmarshal(nil " + e.Type.String() + ")" +} + +func (d *decodeState) unmarshal(v any) error { + rv := reflect.ValueOf(v) + if rv.Kind() != reflect.Pointer || rv.IsNil() { + return &InvalidUnmarshalError{reflect.TypeOf(v)} + } + + d.scan.reset() + d.scanWhile(scanSkipSpace) + // We decode rv not rv.Elem because the Unmarshaler interface + // test must be applied at the top level of the value. + err := d.value(rv) + if err != nil { + return d.addErrorContext(err) + } + return d.savedError +} + +// A Number represents a JSON number literal. +type Number string + +// String returns the literal text of the number. +func (n Number) String() string { return string(n) } + +// Float64 returns the number as a float64. +func (n Number) Float64() (float64, error) { + return strconv.ParseFloat(string(n), 64) +} + +// Int64 returns the number as an int64. +func (n Number) Int64() (int64, error) { + return strconv.ParseInt(string(n), 10, 64) +} + +// An errorContext provides context for type errors during decoding. +type errorContext struct { + Struct reflect.Type + FieldStack []string +} + +// decodeState represents the state while decoding a JSON value. +type decodeState struct { + data []byte + off int // next read offset in data + opcode int // last read result + scan scanner + errorContext *errorContext + savedError error + useNumber bool + disallowUnknownFields bool +} + +// readIndex returns the position of the last byte read. +func (d *decodeState) readIndex() int { + return d.off - 1 +} + +// phasePanicMsg is used as a panic message when we end up with something that +// shouldn't happen. It can indicate a bug in the JSON decoder, or that +// something is editing the data slice while the decoder executes. +const phasePanicMsg = "JSON decoder out of sync - data changing underfoot?" + +func (d *decodeState) init(data []byte) *decodeState { + d.data = data + d.off = 0 + d.savedError = nil + if d.errorContext != nil { + d.errorContext.Struct = nil + // Reuse the allocated space for the FieldStack slice. + d.errorContext.FieldStack = d.errorContext.FieldStack[:0] + } + return d +} + +// saveError saves the first err it is called with, +// for reporting at the end of the unmarshal. +func (d *decodeState) saveError(err error) { + if d.savedError == nil { + d.savedError = d.addErrorContext(err) + } +} + +// addErrorContext returns a new error enhanced with information from d.errorContext +func (d *decodeState) addErrorContext(err error) error { + if d.errorContext != nil && (d.errorContext.Struct != nil || len(d.errorContext.FieldStack) > 0) { + switch err := err.(type) { + case *UnmarshalTypeError: + err.Struct = d.errorContext.Struct.Name() + fieldStack := d.errorContext.FieldStack + if err.Field != "" { + fieldStack = append(fieldStack, err.Field) + } + err.Field = strings.Join(fieldStack, ".") + } + } + return err +} + +// skip scans to the end of what was started. +func (d *decodeState) skip() { + s, data, i := &d.scan, d.data, d.off + depth := len(s.parseState) + for { + op := s.step(s, data[i]) + i++ + if len(s.parseState) < depth { + d.off = i + d.opcode = op + return + } + } +} + +// scanNext processes the byte at d.data[d.off]. +func (d *decodeState) scanNext() { + if d.off < len(d.data) { + d.opcode = d.scan.step(&d.scan, d.data[d.off]) + d.off++ + } else { + d.opcode = d.scan.eof() + d.off = len(d.data) + 1 // mark processed EOF with len+1 + } +} + +// scanWhile processes bytes in d.data[d.off:] until it +// receives a scan code not equal to op. +func (d *decodeState) scanWhile(op int) { + s, data, i := &d.scan, d.data, d.off + for i < len(data) { + newOp := s.step(s, data[i]) + i++ + if newOp != op { + d.opcode = newOp + d.off = i + return + } + } + + d.off = len(data) + 1 // mark processed EOF with len+1 + d.opcode = d.scan.eof() +} + +// rescanLiteral is similar to scanWhile(scanContinue), but it specialises the +// common case where we're decoding a literal. The decoder scans the input +// twice, once for syntax errors and to check the length of the value, and the +// second to perform the decoding. +// +// Only in the second step do we use decodeState to tokenize literals, so we +// know there aren't any syntax errors. We can take advantage of that knowledge, +// and scan a literal's bytes much more quickly. +func (d *decodeState) rescanLiteral() { + data, i := d.data, d.off +Switch: + switch data[i-1] { + case '"': // string + for ; i < len(data); i++ { + switch data[i] { + case '\\': + i++ // escaped char + case '"': + i++ // tokenize the closing quote too + break Switch + } + } + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-': // number + for ; i < len(data); i++ { + switch data[i] { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '.', 'e', 'E', '+', '-': + default: + break Switch + } + } + case 't': // true + i += len("rue") + case 'f': // false + i += len("alse") + case 'n': // null + i += len("ull") + } + if i < len(data) { + d.opcode = stateEndValue(&d.scan, data[i]) + } else { + d.opcode = scanEnd + } + d.off = i + 1 +} + +// value consumes a JSON value from d.data[d.off-1:], decoding into v, and +// reads the following byte ahead. If v is invalid, the value is discarded. +// The first byte of the value has been read already. +func (d *decodeState) value(v reflect.Value) error { + switch d.opcode { + default: + panic(phasePanicMsg) + + case scanBeginArray: + if v.IsValid() { + if err := d.array(v); err != nil { + return err + } + } else { + d.skip() + } + d.scanNext() + + case scanBeginObject: + if v.IsValid() { + if err := d.object(v); err != nil { + return err + } + } else { + d.skip() + } + d.scanNext() + + case scanBeginLiteral: + // All bytes inside literal return scanContinue op code. + start := d.readIndex() + d.rescanLiteral() + + if v.IsValid() { + if err := d.literalStore(d.data[start:d.readIndex()], v, false); err != nil { + return err + } + } + } + return nil +} + +type unquotedValue struct{} + +// valueQuoted is like value but decodes a +// quoted string literal or literal null into an interface value. +// If it finds anything other than a quoted string literal or null, +// valueQuoted returns unquotedValue{}. +func (d *decodeState) valueQuoted() any { + switch d.opcode { + default: + panic(phasePanicMsg) + + case scanBeginArray, scanBeginObject: + d.skip() + d.scanNext() + + case scanBeginLiteral: + v := d.literalInterface() + switch v.(type) { + case nil, string: + return v + } + } + return unquotedValue{} +} + +// indirect walks down v allocating pointers as needed, +// until it gets to a non-pointer. +// If it encounters an Unmarshaler, indirect stops and returns that. +// If decodingNull is true, indirect stops at the first settable pointer so it +// can be set to nil. +func indirect(v reflect.Value, decodingNull bool) (Unmarshaler, encoding.TextUnmarshaler, reflect.Value) { + // Issue #24153 indicates that it is generally not a guaranteed property + // that you may round-trip a reflect.Value by calling Value.Addr().Elem() + // and expect the value to still be settable for values derived from + // unexported embedded struct fields. + // + // The logic below effectively does this when it first addresses the value + // (to satisfy possible pointer methods) and continues to dereference + // subsequent pointers as necessary. + // + // After the first round-trip, we set v back to the original value to + // preserve the original RW flags contained in reflect.Value. + v0 := v + haveAddr := false + + // If v is a named type and is addressable, + // start with its address, so that if the type has pointer methods, + // we find them. + if v.Kind() != reflect.Pointer && v.Type().Name() != "" && v.CanAddr() { + haveAddr = true + v = v.Addr() + } + for { + // Load value from interface, but only if the result will be + // usefully addressable. + if v.Kind() == reflect.Interface && !v.IsNil() { + e := v.Elem() + if e.Kind() == reflect.Pointer && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Pointer) { + haveAddr = false + v = e + continue + } + } + + if v.Kind() != reflect.Pointer { + break + } + + if decodingNull && v.CanSet() { + break + } + + // Prevent infinite loop if v is an interface pointing to its own address: + // var v any + // v = &v + if v.Elem().Kind() == reflect.Interface && v.Elem().Elem().Equal(v) { + v = v.Elem() + break + } + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + if v.Type().NumMethod() > 0 && v.CanInterface() { + if u, ok := v.Interface().(Unmarshaler); ok { + return u, nil, reflect.Value{} + } + if !decodingNull { + if u, ok := v.Interface().(encoding.TextUnmarshaler); ok { + return nil, u, reflect.Value{} + } + } + } + + if haveAddr { + v = v0 // restore original value after round-trip Value.Addr().Elem() + haveAddr = false + } else { + v = v.Elem() + } + } + return nil, nil, v +} + +// array consumes an array from d.data[d.off-1:], decoding into v. +// The first byte of the array ('[') has been read already. +func (d *decodeState) array(v reflect.Value) error { + // Check for unmarshaler. + u, ut, pv := indirect(v, false) + if u != nil { + start := d.readIndex() + d.skip() + return u.UnmarshalJSON(d.data[start:d.off]) + } + if ut != nil { + d.saveError(&UnmarshalTypeError{Value: "array", Type: v.Type(), Offset: int64(d.off)}) + d.skip() + return nil + } + v = pv + + // Check type of target. + switch v.Kind() { + case reflect.Interface: + if v.NumMethod() == 0 { + // Decoding into nil interface? Switch to non-reflect code. + ai := d.arrayInterface() + v.Set(reflect.ValueOf(ai)) + return nil + } + // Otherwise it's invalid. + fallthrough + default: + d.saveError(&UnmarshalTypeError{Value: "array", Type: v.Type(), Offset: int64(d.off)}) + d.skip() + return nil + case reflect.Array, reflect.Slice: + break + } + + i := 0 + for { + // Look ahead for ] - can only happen on first iteration. + d.scanWhile(scanSkipSpace) + if d.opcode == scanEndArray { + break + } + + // Expand slice length, growing the slice if necessary. + if v.Kind() == reflect.Slice { + if i >= v.Cap() { + v.Grow(1) + } + if i >= v.Len() { + v.SetLen(i + 1) + } + } + + if i < v.Len() { + // Decode into element. + if err := d.value(v.Index(i)); err != nil { + return err + } + } else { + // Ran out of fixed array: skip. + if err := d.value(reflect.Value{}); err != nil { + return err + } + } + i++ + + // Next token must be , or ]. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.opcode == scanEndArray { + break + } + if d.opcode != scanArrayValue { + panic(phasePanicMsg) + } + } + + if i < v.Len() { + if v.Kind() == reflect.Array { + for ; i < v.Len(); i++ { + v.Index(i).SetZero() // zero remainder of array + } + } else { + v.SetLen(i) // truncate the slice + } + } + if i == 0 && v.Kind() == reflect.Slice { + v.Set(reflect.MakeSlice(v.Type(), 0, 0)) + } + return nil +} + +var nullLiteral = []byte("null") + +// SHIM(reflect): reflect.TypeFor[T]() reflect.T +var textUnmarshalerType = shims.TypeFor[encoding.TextUnmarshaler]() + +// object consumes an object from d.data[d.off-1:], decoding into v. +// The first byte ('{') of the object has been read already. +func (d *decodeState) object(v reflect.Value) error { + // Check for unmarshaler. + u, ut, pv := indirect(v, false) + if u != nil { + start := d.readIndex() + d.skip() + return u.UnmarshalJSON(d.data[start:d.off]) + } + if ut != nil { + d.saveError(&UnmarshalTypeError{Value: "object", Type: v.Type(), Offset: int64(d.off)}) + d.skip() + return nil + } + v = pv + t := v.Type() + + // Decoding into nil interface? Switch to non-reflect code. + if v.Kind() == reflect.Interface && v.NumMethod() == 0 { + oi := d.objectInterface() + v.Set(reflect.ValueOf(oi)) + return nil + } + + var fields structFields + + // Check type of target: + // struct or + // map[T1]T2 where T1 is string, an integer type, + // or an encoding.TextUnmarshaler + switch v.Kind() { + case reflect.Map: + // Map key must either have string kind, have an integer kind, + // or be an encoding.TextUnmarshaler. + switch t.Key().Kind() { + case reflect.String, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + default: + if !reflect.PointerTo(t.Key()).Implements(textUnmarshalerType) { + d.saveError(&UnmarshalTypeError{Value: "object", Type: t, Offset: int64(d.off)}) + d.skip() + return nil + } + } + if v.IsNil() { + v.Set(reflect.MakeMap(t)) + } + case reflect.Struct: + fields = cachedTypeFields(t) + // ok + default: + d.saveError(&UnmarshalTypeError{Value: "object", Type: t, Offset: int64(d.off)}) + d.skip() + return nil + } + + var mapElem reflect.Value + var origErrorContext errorContext + if d.errorContext != nil { + origErrorContext = *d.errorContext + } + + for { + // Read opening " of string key or closing }. + d.scanWhile(scanSkipSpace) + if d.opcode == scanEndObject { + // closing } - can only happen on first iteration. + break + } + if d.opcode != scanBeginLiteral { + panic(phasePanicMsg) + } + + // Read key. + start := d.readIndex() + d.rescanLiteral() + item := d.data[start:d.readIndex()] + key, ok := unquoteBytes(item) + if !ok { + panic(phasePanicMsg) + } + + // Figure out field corresponding to key. + var subv reflect.Value + destring := false // whether the value is wrapped in a string to be decoded first + + if v.Kind() == reflect.Map { + elemType := t.Elem() + if !mapElem.IsValid() { + mapElem = reflect.New(elemType).Elem() + } else { + mapElem.SetZero() + } + subv = mapElem + } else { + f := fields.byExactName[string(key)] + if f == nil { + f = fields.byFoldedName[string(foldName(key))] + } + if f != nil { + subv = v + destring = f.quoted + if d.errorContext == nil { + d.errorContext = new(errorContext) + } + for i, ind := range f.index { + if subv.Kind() == reflect.Pointer { + if subv.IsNil() { + // If a struct embeds a pointer to an unexported type, + // it is not possible to set a newly allocated value + // since the field is unexported. + // + // See https://golang.org/issue/21357 + if !subv.CanSet() { + d.saveError(fmt.Errorf("json: cannot set embedded pointer to unexported struct: %v", subv.Type().Elem())) + // Invalidate subv to ensure d.value(subv) skips over + // the JSON value without assigning it to subv. + subv = reflect.Value{} + destring = false + break + } + subv.Set(reflect.New(subv.Type().Elem())) + } + subv = subv.Elem() + } + if i < len(f.index)-1 { + d.errorContext.FieldStack = append( + d.errorContext.FieldStack, + subv.Type().Field(ind).Name, + ) + } + subv = subv.Field(ind) + } + d.errorContext.Struct = t + d.errorContext.FieldStack = append(d.errorContext.FieldStack, f.name) + } else if d.disallowUnknownFields { + d.saveError(fmt.Errorf("json: unknown field %q", key)) + } + } + + // Read : before value. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.opcode != scanObjectKey { + panic(phasePanicMsg) + } + d.scanWhile(scanSkipSpace) + + if destring { + switch qv := d.valueQuoted().(type) { + case nil: + if err := d.literalStore(nullLiteral, subv, false); err != nil { + return err + } + case string: + if err := d.literalStore([]byte(qv), subv, true); err != nil { + return err + } + default: + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal unquoted value into %v", subv.Type())) + } + } else { + if err := d.value(subv); err != nil { + return err + } + } + + // Write value back to map; + // if using struct, subv points into struct already. + if v.Kind() == reflect.Map { + kt := t.Key() + var kv reflect.Value + if reflect.PointerTo(kt).Implements(textUnmarshalerType) { + kv = reflect.New(kt) + if err := d.literalStore(item, kv, true); err != nil { + return err + } + kv = kv.Elem() + } else { + switch kt.Kind() { + case reflect.String: + kv = reflect.New(kt).Elem() + kv.SetString(string(key)) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + s := string(key) + n, err := strconv.ParseInt(s, 10, 64) + // SHIM(reflect): reflect.Type.OverflowInt(int64) bool + okt := shims.OverflowableType{Type: kt} + if err != nil || okt.OverflowInt(n) { + d.saveError(&UnmarshalTypeError{Value: "number " + s, Type: kt, Offset: int64(start + 1)}) + break + } + kv = reflect.New(kt).Elem() + kv.SetInt(n) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + s := string(key) + n, err := strconv.ParseUint(s, 10, 64) + // SHIM(reflect): reflect.Type.OverflowUint(uint64) bool + okt := shims.OverflowableType{Type: kt} + if err != nil || okt.OverflowUint(n) { + d.saveError(&UnmarshalTypeError{Value: "number " + s, Type: kt, Offset: int64(start + 1)}) + break + } + kv = reflect.New(kt).Elem() + kv.SetUint(n) + default: + panic("json: Unexpected key type") // should never occur + } + } + if kv.IsValid() { + v.SetMapIndex(kv, subv) + } + } + + // Next token must be , or }. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.errorContext != nil { + // Reset errorContext to its original state. + // Keep the same underlying array for FieldStack, to reuse the + // space and avoid unnecessary allocs. + d.errorContext.FieldStack = d.errorContext.FieldStack[:len(origErrorContext.FieldStack)] + d.errorContext.Struct = origErrorContext.Struct + } + if d.opcode == scanEndObject { + break + } + if d.opcode != scanObjectValue { + panic(phasePanicMsg) + } + } + return nil +} + +// convertNumber converts the number literal s to a float64 or a Number +// depending on the setting of d.useNumber. +func (d *decodeState) convertNumber(s string) (any, error) { + if d.useNumber { + return Number(s), nil + } + f, err := strconv.ParseFloat(s, 64) + if err != nil { + // SHIM(reflect): reflect.TypeFor[T]() reflect.Type + return nil, &UnmarshalTypeError{Value: "number " + s, Type: shims.TypeFor[float64](), Offset: int64(d.off)} + } + return f, nil +} + +// SHIM(reflect): TypeFor[T]() reflect.Type +var numberType = shims.TypeFor[Number]() + +// literalStore decodes a literal stored in item into v. +// +// fromQuoted indicates whether this literal came from unwrapping a +// string from the ",string" struct tag option. this is used only to +// produce more helpful error messages. +func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool) error { + // Check for unmarshaler. + if len(item) == 0 { + // Empty string given. + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + return nil + } + isNull := item[0] == 'n' // null + u, ut, pv := indirect(v, isNull) + if u != nil { + return u.UnmarshalJSON(item) + } + if ut != nil { + if item[0] != '"' { + if fromQuoted { + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + return nil + } + val := "number" + switch item[0] { + case 'n': + val = "null" + case 't', 'f': + val = "bool" + } + d.saveError(&UnmarshalTypeError{Value: val, Type: v.Type(), Offset: int64(d.readIndex())}) + return nil + } + s, ok := unquoteBytes(item) + if !ok { + if fromQuoted { + return fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()) + } + panic(phasePanicMsg) + } + return ut.UnmarshalText(s) + } + + v = pv + + switch c := item[0]; c { + case 'n': // null + // The main parser checks that only true and false can reach here, + // but if this was a quoted string input, it could be anything. + if fromQuoted && string(item) != "null" { + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + break + } + switch v.Kind() { + case reflect.Interface, reflect.Pointer, reflect.Map, reflect.Slice: + v.SetZero() + // otherwise, ignore null for primitives/string + } + case 't', 'f': // true, false + value := item[0] == 't' + // The main parser checks that only true and false can reach here, + // but if this was a quoted string input, it could be anything. + if fromQuoted && string(item) != "true" && string(item) != "false" { + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + break + } + switch v.Kind() { + default: + if fromQuoted { + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + } else { + d.saveError(&UnmarshalTypeError{Value: "bool", Type: v.Type(), Offset: int64(d.readIndex())}) + } + case reflect.Bool: + v.SetBool(value) + case reflect.Interface: + if v.NumMethod() == 0 { + v.Set(reflect.ValueOf(value)) + } else { + d.saveError(&UnmarshalTypeError{Value: "bool", Type: v.Type(), Offset: int64(d.readIndex())}) + } + } + + case '"': // string + s, ok := unquoteBytes(item) + if !ok { + if fromQuoted { + return fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()) + } + panic(phasePanicMsg) + } + switch v.Kind() { + default: + d.saveError(&UnmarshalTypeError{Value: "string", Type: v.Type(), Offset: int64(d.readIndex())}) + case reflect.Slice: + if v.Type().Elem().Kind() != reflect.Uint8 { + d.saveError(&UnmarshalTypeError{Value: "string", Type: v.Type(), Offset: int64(d.readIndex())}) + break + } + b := make([]byte, base64.StdEncoding.DecodedLen(len(s))) + n, err := base64.StdEncoding.Decode(b, s) + if err != nil { + d.saveError(err) + break + } + v.SetBytes(b[:n]) + case reflect.String: + t := string(s) + if v.Type() == numberType && !isValidNumber(t) { + return fmt.Errorf("json: invalid number literal, trying to unmarshal %q into Number", item) + } + v.SetString(t) + case reflect.Interface: + if v.NumMethod() == 0 { + v.Set(reflect.ValueOf(string(s))) + } else { + d.saveError(&UnmarshalTypeError{Value: "string", Type: v.Type(), Offset: int64(d.readIndex())}) + } + } + + default: // number + if c != '-' && (c < '0' || c > '9') { + if fromQuoted { + return fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()) + } + panic(phasePanicMsg) + } + switch v.Kind() { + default: + if v.Kind() == reflect.String && v.Type() == numberType { + // s must be a valid number, because it's + // already been tokenized. + v.SetString(string(item)) + break + } + if fromQuoted { + return fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()) + } + d.saveError(&UnmarshalTypeError{Value: "number", Type: v.Type(), Offset: int64(d.readIndex())}) + case reflect.Interface: + n, err := d.convertNumber(string(item)) + if err != nil { + d.saveError(err) + break + } + if v.NumMethod() != 0 { + d.saveError(&UnmarshalTypeError{Value: "number", Type: v.Type(), Offset: int64(d.readIndex())}) + break + } + v.Set(reflect.ValueOf(n)) + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + n, err := strconv.ParseInt(string(item), 10, 64) + if err != nil || v.OverflowInt(n) { + d.saveError(&UnmarshalTypeError{Value: "number " + string(item), Type: v.Type(), Offset: int64(d.readIndex())}) + break + } + v.SetInt(n) + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + n, err := strconv.ParseUint(string(item), 10, 64) + if err != nil || v.OverflowUint(n) { + d.saveError(&UnmarshalTypeError{Value: "number " + string(item), Type: v.Type(), Offset: int64(d.readIndex())}) + break + } + v.SetUint(n) + + case reflect.Float32, reflect.Float64: + n, err := strconv.ParseFloat(string(item), v.Type().Bits()) + if err != nil || v.OverflowFloat(n) { + d.saveError(&UnmarshalTypeError{Value: "number " + string(item), Type: v.Type(), Offset: int64(d.readIndex())}) + break + } + v.SetFloat(n) + } + } + return nil +} + +// The xxxInterface routines build up a value to be stored +// in an empty interface. They are not strictly necessary, +// but they avoid the weight of reflection in this common case. + +// valueInterface is like value but returns any. +func (d *decodeState) valueInterface() (val any) { + switch d.opcode { + default: + panic(phasePanicMsg) + case scanBeginArray: + val = d.arrayInterface() + d.scanNext() + case scanBeginObject: + val = d.objectInterface() + d.scanNext() + case scanBeginLiteral: + val = d.literalInterface() + } + return +} + +// arrayInterface is like array but returns []any. +func (d *decodeState) arrayInterface() []any { + var v = make([]any, 0) + for { + // Look ahead for ] - can only happen on first iteration. + d.scanWhile(scanSkipSpace) + if d.opcode == scanEndArray { + break + } + + v = append(v, d.valueInterface()) + + // Next token must be , or ]. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.opcode == scanEndArray { + break + } + if d.opcode != scanArrayValue { + panic(phasePanicMsg) + } + } + return v +} + +// objectInterface is like object but returns map[string]any. +func (d *decodeState) objectInterface() map[string]any { + m := make(map[string]any) + for { + // Read opening " of string key or closing }. + d.scanWhile(scanSkipSpace) + if d.opcode == scanEndObject { + // closing } - can only happen on first iteration. + break + } + if d.opcode != scanBeginLiteral { + panic(phasePanicMsg) + } + + // Read string key. + start := d.readIndex() + d.rescanLiteral() + item := d.data[start:d.readIndex()] + key, ok := unquote(item) + if !ok { + panic(phasePanicMsg) + } + + // Read : before value. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.opcode != scanObjectKey { + panic(phasePanicMsg) + } + d.scanWhile(scanSkipSpace) + + // Read value. + m[key] = d.valueInterface() + + // Next token must be , or }. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.opcode == scanEndObject { + break + } + if d.opcode != scanObjectValue { + panic(phasePanicMsg) + } + } + return m +} + +// literalInterface consumes and returns a literal from d.data[d.off-1:] and +// it reads the following byte ahead. The first byte of the literal has been +// read already (that's how the caller knows it's a literal). +func (d *decodeState) literalInterface() any { + // All bytes inside literal return scanContinue op code. + start := d.readIndex() + d.rescanLiteral() + + item := d.data[start:d.readIndex()] + + switch c := item[0]; c { + case 'n': // null + return nil + + case 't', 'f': // true, false + return c == 't' + + case '"': // string + s, ok := unquote(item) + if !ok { + panic(phasePanicMsg) + } + return s + + default: // number + if c != '-' && (c < '0' || c > '9') { + panic(phasePanicMsg) + } + n, err := d.convertNumber(string(item)) + if err != nil { + d.saveError(err) + } + return n + } +} + +// getu4 decodes \uXXXX from the beginning of s, returning the hex value, +// or it returns -1. +func getu4(s []byte) rune { + if len(s) < 6 || s[0] != '\\' || s[1] != 'u' { + return -1 + } + var r rune + for _, c := range s[2:6] { + switch { + case '0' <= c && c <= '9': + c = c - '0' + case 'a' <= c && c <= 'f': + c = c - 'a' + 10 + case 'A' <= c && c <= 'F': + c = c - 'A' + 10 + default: + return -1 + } + r = r*16 + rune(c) + } + return r +} + +// unquote converts a quoted JSON string literal s into an actual string t. +// The rules are different than for Go, so cannot use strconv.Unquote. +func unquote(s []byte) (t string, ok bool) { + s, ok = unquoteBytes(s) + t = string(s) + return +} + +// unquoteBytes should be an internal detail, +// but widely used packages access it using linkname. +// Notable members of the hall of shame include: +// - github.com/bytedance/sonic +// +// Do not remove or change the type signature. +// See go.dev/issue/67401. +// +//go:linkname unquoteBytes +func unquoteBytes(s []byte) (t []byte, ok bool) { + if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' { + return + } + s = s[1 : len(s)-1] + + // Check for unusual characters. If there are none, + // then no unquoting is needed, so return a slice of the + // original bytes. + r := 0 + for r < len(s) { + c := s[r] + if c == '\\' || c == '"' || c < ' ' { + break + } + if c < utf8.RuneSelf { + r++ + continue + } + rr, size := utf8.DecodeRune(s[r:]) + if rr == utf8.RuneError && size == 1 { + break + } + r += size + } + if r == len(s) { + return s, true + } + + b := make([]byte, len(s)+2*utf8.UTFMax) + w := copy(b, s[0:r]) + for r < len(s) { + // Out of room? Can only happen if s is full of + // malformed UTF-8 and we're replacing each + // byte with RuneError. + if w >= len(b)-2*utf8.UTFMax { + nb := make([]byte, (len(b)+utf8.UTFMax)*2) + copy(nb, b[0:w]) + b = nb + } + switch c := s[r]; { + case c == '\\': + r++ + if r >= len(s) { + return + } + switch s[r] { + default: + return + case '"', '\\', '/', '\'': + b[w] = s[r] + r++ + w++ + case 'b': + b[w] = '\b' + r++ + w++ + case 'f': + b[w] = '\f' + r++ + w++ + case 'n': + b[w] = '\n' + r++ + w++ + case 'r': + b[w] = '\r' + r++ + w++ + case 't': + b[w] = '\t' + r++ + w++ + case 'u': + r-- + rr := getu4(s[r:]) + if rr < 0 { + return + } + r += 6 + if utf16.IsSurrogate(rr) { + rr1 := getu4(s[r:]) + if dec := utf16.DecodeRune(rr, rr1); dec != unicode.ReplacementChar { + // A valid pair; consume. + r += 6 + w += utf8.EncodeRune(b[w:], dec) + break + } + // Invalid surrogate; fall back to replacement rune. + rr = unicode.ReplacementChar + } + w += utf8.EncodeRune(b[w:], rr) + } + + // Quote, control characters are invalid. + case c == '"', c < ' ': + return + + // ASCII + case c < utf8.RuneSelf: + b[w] = c + r++ + w++ + + // Coerce to well-formed UTF-8. + default: + rr, size := utf8.DecodeRune(s[r:]) + r += size + w += utf8.EncodeRune(b[w:], rr) + } + } + return b[0:w], true +} diff --git a/internal/encoding/json/encode.go b/internal/encoding/json/encode.go new file mode 100644 index 0000000..d258054 --- /dev/null +++ b/internal/encoding/json/encode.go @@ -0,0 +1,1395 @@ +// Vendored from Go 1.24.0-pre-release +// To find alterations, check package shims, and comments beginning in SHIM(). +// +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package json implements encoding and decoding of JSON as defined in +// RFC 7159. The mapping between JSON and Go values is described +// in the documentation for the Marshal and Unmarshal functions. +// +// See "JSON and Go" for an introduction to this package: +// https://golang.org/doc/articles/json_and_go.html +package json + +import ( + "bytes" + "cmp" + "encoding" + "encoding/base64" + "fmt" + "github.com/imagekit-developer/imagekit-go/internal/encoding/json/sentinel" + "github.com/imagekit-developer/imagekit-go/internal/encoding/json/shims" + "math" + "reflect" + "slices" + "strconv" + "strings" + "sync" + "unicode" + "unicode/utf8" + _ "unsafe" // for linkname +) + +// Marshal returns the JSON encoding of v. +// +// Marshal traverses the value v recursively. +// If an encountered value implements [Marshaler] +// and is not a nil pointer, Marshal calls [Marshaler.MarshalJSON] +// to produce JSON. If no [Marshaler.MarshalJSON] method is present but the +// value implements [encoding.TextMarshaler] instead, Marshal calls +// [encoding.TextMarshaler.MarshalText] and encodes the result as a JSON string. +// The nil pointer exception is not strictly necessary +// but mimics a similar, necessary exception in the behavior of +// [Unmarshaler.UnmarshalJSON]. +// +// Otherwise, Marshal uses the following type-dependent default encodings: +// +// Boolean values encode as JSON booleans. +// +// Floating point, integer, and [Number] values encode as JSON numbers. +// NaN and +/-Inf values will return an [UnsupportedValueError]. +// +// String values encode as JSON strings coerced to valid UTF-8, +// replacing invalid bytes with the Unicode replacement rune. +// So that the JSON will be safe to embed inside HTML