diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
new file mode 100644
index 0000000..e6b830c
--- /dev/null
+++ b/.github/workflows/coverage.yml
@@ -0,0 +1,33 @@
+name: "Go Coverage"
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - main
+ pull_request:
+
+jobs:
+ coverage:
+ # Ignore drafts
+ if: github.event.pull_request.draft == false
+ name: Go test with coverage
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 10
+
+ - name: Set up Golang
+ uses: actions/setup-go@v5
+ with:
+ go-version: '1.22'
+
+ - name: Install dependencies
+ run: go mod tidy
+
+ - uses: gwatts/go-coverage-action@v2
+ id: coverage
+ with:
+ coverage-threshold: 60
+ cover-pkg: ./...
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..7847ffc
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,29 @@
+name: Create Release
+
+on:
+ push:
+ tags:
+ - 'v[0-9]+.[0-9]+.[0-9]+'
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+
+ - name: Set up Golang
+ uses: actions/setup-go@v5
+ with:
+ go-version: '1.22'
+
+ - name: Install dependencies
+ run: go mod tidy
+
+ - name: Testing
+ run: go test -v ./...
+
+ - name: Publish to pkg.go.dev
+ run: |
+ echo "VERSION=${{ github.ref_name }}"
+ GOPROXY=proxy.golang.org go list -m github.com/HawAPI/go-sdk@${{ github.ref_name }}
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
new file mode 100644
index 0000000..26c099a
--- /dev/null
+++ b/.idea/dataSources.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ postgresql
+ true
+ org.postgresql.Driver
+ jdbc:postgresql://localhost:5432/hawapi
+ $ProjectFileDir$
+
+
+
\ No newline at end of file
diff --git a/.idea/go-sdk.iml b/.idea/go-sdk.iml
new file mode 100644
index 0000000..5e764c4
--- /dev/null
+++ b/.idea/go-sdk.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..d7faeaa
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/markdown.xml b/.idea/markdown.xml
new file mode 100644
index 0000000..f6d2542
--- /dev/null
+++ b/.idea/markdown.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..d74548e
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 1837063..e6211e3 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,117 @@
-# go-sdk
+# HawAPI - go-sdk
+
HawAPI SDK for Golang
+
+- [API Docs](https://hawapi.theproject.id/docs/)
+- [SDK Docs](https://pkg.go.dev/github.com/HawAPI/go-sdk)
+
+## Topics
+
+- [Installation](#installation)
+- [Usage](#usage)
+ - [Init client](#init-client)
+ - [Fetch information](#fetch-information)
+ - [Error handling](#error-handling)
+
+## Installation
+
+```
+go get github.com/HawAPI/go-sdk@latest
+```
+
+## Usage
+
+- [See examples](./examples)
+
+### Init client
+
+```go
+package main
+
+import (
+ "fmt"
+
+ "github.com/HawAPI/go-sdk"
+)
+
+func main() {
+ // Create a new client with default options
+ client := hawapi.NewClient()
+
+ // Create client with custom options
+ client = hawapi.NewClientWithOpts(hawapi.Options{
+ Endpoint: "http://localhost:8080/api",
+ // When using 'WithOpts' or 'NewClientWithOpts' the value of
+ // 'UseInMemoryCache' will be set to false
+ UseInMemoryCache: true,
+ // Version
+ // Language
+ // Token
+ // ...
+ })
+
+ // You can also change the options later
+ client.WithOpts(hawapi.Options{
+ Language: "pt-BR",
+ // When using 'WithOpts' or 'NewClientWithOpts' the value of
+ // 'UseInMemoryCache' will be set to false
+ UseInMemoryCache: true,
+ })
+}
+```
+
+### Fetch information
+
+```go
+package main
+
+import (
+ "fmt"
+
+ "github.com/HawAPI/go-sdk"
+)
+
+func main() {
+ client := hawapi.NewClient()
+
+ res, err := client.ListActors()
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res)
+}
+```
+
+### Error handling
+
+- Check out the [hawapi.ErrorResponse](./pkg/hawapi/error.go)
+
+```go
+package main
+
+import (
+ "fmt"
+
+ "github.com/HawAPI/go-sdk"
+ "github.com/google/uuid"
+)
+
+func main() {
+ client := hawapi.NewClient()
+
+ id, _ := uuid.Parse("")
+ res, err := client.FindActor(id)
+ if err != nil {
+ // If the error is coming from the API request,
+ // it'll be of type hawapi.ErrorResponse.
+ if resErr, ok := err.(hawapi.ErrorResponse); ok {
+ fmt.Printf("API error %d Message: %s\n", resErr.Code, resErr.Message)
+ } else {
+ fmt.Println("SDK error:", err)
+ }
+ }
+
+ fmt.Println(res)
+}
+```
\ No newline at end of file
diff --git a/examples/create.go b/examples/create.go
new file mode 100644
index 0000000..51b9b4a
--- /dev/null
+++ b/examples/create.go
@@ -0,0 +1,25 @@
+package main
+
+import (
+ "fmt"
+
+ "github.com/HawAPI/go-sdk/pkg/hawapi"
+)
+
+func main() {
+ client := hawapi.NewClient()
+ client.WithOpts(hawapi.Options{
+ // JWT auth is required when performing any POST, PATCH and DELETE requests
+ Token: "",
+ })
+
+ actor := hawapi.CreateActor{
+ // ...
+ }
+ res, err := client.CreateActor(actor)
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res.FirstName)
+}
diff --git a/examples/list.go b/examples/list.go
new file mode 100644
index 0000000..3fa788a
--- /dev/null
+++ b/examples/list.go
@@ -0,0 +1,28 @@
+package main
+
+import (
+ "fmt"
+
+ "github.com/HawAPI/go-sdk/pkg/hawapi"
+)
+
+func main() {
+ // Create a new client with default options
+ client := hawapi.NewClient()
+
+ // Override options
+ client.WithOpts(hawapi.Options{
+ Endpoint: "http://localhost:8080/api",
+ // When using 'WithOpts' or 'NewClientWithOpts' the value of
+ // 'UseInMemoryCache' will be set to false
+ UseInMemoryCache: true,
+ })
+
+ res, err := client.ListActors()
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(res)
+ fmt.Println(len(res.Data))
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..1ceb5da
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,14 @@
+module github.com/HawAPI/go-sdk
+
+go 1.22.0
+
+require (
+ github.com/fatih/color v1.17.0
+ github.com/google/uuid v1.6.0
+)
+
+require (
+ github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ golang.org/x/sys v0.18.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..152870e
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,13 @@
+github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
+github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go
new file mode 100644
index 0000000..278554e
--- /dev/null
+++ b/pkg/cache/cache.go
@@ -0,0 +1,49 @@
+package cache
+
+// Cache is a simple key / value cache
+type Cache interface {
+ Get(key string) (any, bool)
+ Set(key string, value any)
+ Del(key string)
+ Size() int
+ Clear() int
+}
+
+type memoryCache struct {
+ cache map[string]any
+}
+
+// NewMemoryCache creates a new Cache
+func NewMemoryCache() Cache {
+ return &memoryCache{
+ cache: make(map[string]any),
+ }
+}
+
+// Get will try to get associated with a key from the cache, if present
+func (c *memoryCache) Get(key string) (any, bool) {
+ v, ok := c.cache[key]
+ return v, ok
+}
+
+// Set will store a key-value pair in the cache
+func (c *memoryCache) Set(key string, value any) {
+ c.cache[key] = value
+}
+
+// Del will remove a key and its associated value from the cache.
+func (c *memoryCache) Del(key string) {
+ delete(c.cache, key)
+}
+
+// Size will return the current number of entries in the cache.
+func (c *memoryCache) Size() int {
+ return len(c.cache)
+}
+
+// Clear will empty the cache, removing all stored key-value pairs.
+func (c *memoryCache) Clear() int {
+ count := len(c.cache)
+ c.cache = make(map[string]any)
+ return count
+}
diff --git a/pkg/hawapi/actor.go b/pkg/hawapi/actor.go
new file mode 100644
index 0000000..f07cbff
--- /dev/null
+++ b/pkg/hawapi/actor.go
@@ -0,0 +1,148 @@
+package hawapi
+
+import (
+ "github.com/google/uuid"
+)
+
+const actorOrigin = "actors"
+
+type Social struct {
+ Social string `json:"social,omitempty"`
+ Handle string `json:"handle,omitempty"`
+ URL string `json:"url,omitempty"`
+}
+
+type Actor struct {
+ UUID uuid.UUID `json:"uuid"`
+ Href string `json:"href"`
+ FirstName string `json:"first_name"`
+ LastName string `json:"last_name"`
+ Nicknames []string `json:"nicknames,omitempty"`
+ Socials []Social `json:"socials,omitempty"`
+ Nationality string `json:"nationality,omitempty"`
+ BirthDate string `json:"birth_date,omitempty"`
+ DeathDate string `json:"death_date,omitempty"`
+ Gender int `json:"gender,omitempty"`
+ Seasons []string `json:"seasons,omitempty"`
+ Awards []string `json:"awards,omitempty"`
+ Character string `json:"character"`
+ Thumbnail string `json:"thumbnail,omitempty"`
+ Images []string `json:"images,omitempty"`
+ Sources []string `json:"sources,omitempty"`
+ CreatedAt string `json:"created_at"`
+ UpdatedAt string `json:"updated_at"`
+}
+
+type CreateActor struct {
+ FirstName string `json:"first_name,omitempty"`
+ LastName string `json:"last_name,omitempty"`
+ Nicknames []string `json:"nicknames,omitempty"`
+ Socials []Social `json:"socials,omitempty"`
+ Nationality string `json:"nationality,omitempty"`
+ BirthDate string `json:"birth_date,omitempty"`
+ DeathDate string `json:"death_date,omitempty"`
+ Gender int `json:"gender,omitempty"`
+ Seasons []string `json:"seasons,omitempty"`
+ Awards []string `json:"awards,omitempty"`
+ Character string `json:"character,omitempty"`
+ Thumbnail string `json:"thumbnail,omitempty"`
+ Images []string `json:"images,omitempty"`
+ Sources []string `json:"sources,omitempty"`
+}
+
+type PatchActor = CreateActor
+
+type ActorResponse struct {
+ BaseResponse
+ Data Actor `json:"data"`
+}
+
+type ActorListResponse struct {
+ BaseResponse
+ Data []Actor `json:"data"`
+}
+
+// ListActors will get all actors
+func (c *Client) ListActors(options ...QueryOptions) (ActorListResponse, error) {
+ var actors []Actor
+ var res ActorListResponse
+
+ doRes, err := c.doGetRequest(actorOrigin, options, &actors)
+ if err != nil {
+ return res, err
+ }
+
+ res = ActorListResponse{
+ BaseResponse: doRes,
+ Data: actors,
+ }
+
+ return res, nil
+}
+
+// FindActor will get a single item by uuid
+func (c *Client) FindActor(id uuid.UUID) (ActorResponse, error) {
+ var actor Actor
+ var res ActorResponse
+
+ doRes, err := c.doGetRequest(actorOrigin+"/"+id.String(), nil, &actor)
+ if err != nil {
+ return res, err
+ }
+
+ res = ActorResponse{
+ BaseResponse: doRes,
+ Data: actor,
+ }
+
+ return res, nil
+}
+
+func (c *Client) RandomActor() (ActorResponse, error) {
+ var actor Actor
+ var res ActorResponse
+
+ doRes, err := c.doGetRequest(actorOrigin+"/random", nil, &actor)
+ if err != nil {
+ return res, err
+ }
+
+ res = ActorResponse{
+ BaseResponse: doRes,
+ Data: actor,
+ }
+
+ return res, nil
+}
+
+func (c *Client) CreateActor(s CreateActor) (Actor, error) {
+ var actor Actor
+
+ err := c.doPostRequest(actorOrigin, s, &actor)
+ if err != nil {
+ return actor, err
+ }
+
+ return actor, nil
+}
+
+func (c *Client) PatchActor(id uuid.UUID, p PatchActor) (Actor, error) {
+ var actor Actor
+
+ err := c.doPatchRequest(actorOrigin+"/"+id.String(), &p)
+ if err != nil {
+ return actor, err
+ }
+
+ res, err := c.FindActor(id)
+ if err != nil {
+ return actor, err
+ }
+
+ actor = res.Data
+ return actor, nil
+}
+
+func (c *Client) DeleteActor(id uuid.UUID) error {
+ return c.doDeleteRequest(actorOrigin + "/" + id.String())
+}
diff --git a/pkg/hawapi/character.go b/pkg/hawapi/character.go
new file mode 100644
index 0000000..a622754
--- /dev/null
+++ b/pkg/hawapi/character.go
@@ -0,0 +1,134 @@
+package hawapi
+
+import (
+ "github.com/google/uuid"
+)
+
+const characterOrigin = "characters"
+
+type Character struct {
+ Uuid uuid.UUID `json:"uuid"`
+ Href string `json:"href"`
+ FirstName string `json:"first_name"`
+ LastName string `json:"last_name"`
+ Nicknames []string `json:"nicknames,omitempty"`
+ Gender int `json:"gender"`
+ Actor string `json:"actor"`
+ BirthDate string `json:"birth_date,omitempty"`
+ DeathDate string `json:"death_date,omitempty"`
+ Thumbnail string `json:"thumbnail"`
+ Images []string `json:"images,omitempty"`
+ Sources []string `json:"sources,omitempty"`
+ CreatedAt string `json:"created_at"`
+ UpdatedAt string `json:"updated_at"`
+}
+
+type CreateCharacter struct {
+ FirstName string `json:"first_name,omitempty"`
+ LastName string `json:"last_name,omitempty"`
+ Nicknames []string `json:"nicknames,omitempty"`
+ Gender int `json:"gender,omitempty"`
+ Actor string `json:"actor,omitempty"`
+ BirthDate string `json:"birth_date,omitempty"`
+ DeathDate string `json:"death_date,omitempty"`
+ Thumbnail string `json:"thumbnail,omitempty"`
+ Images []string `json:"images,omitempty"`
+ Sources []string `json:"sources,omitempty"`
+}
+
+type PatchCharacter = CreateCharacter
+
+type CharacterResponse struct {
+ BaseResponse
+ Data Character `json:"data"`
+}
+
+type CharacterListResponse struct {
+ BaseResponse
+ Data []Character `json:"data"`
+}
+
+// ListCharacters will get all characters
+func (c *Client) ListCharacters(options ...QueryOptions) (CharacterListResponse, error) {
+ var characters []Character
+ var res CharacterListResponse
+
+ doRes, err := c.doGetRequest(characterOrigin, options, &characters)
+ if err != nil {
+ return res, err
+ }
+
+ res = CharacterListResponse{
+ BaseResponse: doRes,
+ Data: characters,
+ }
+
+ return res, nil
+}
+
+// FindCharacter will get a single item by uuid
+func (c *Client) FindCharacter(id uuid.UUID) (CharacterResponse, error) {
+ var character Character
+ var res CharacterResponse
+
+ doRes, err := c.doGetRequest(characterOrigin+"/"+id.String(), nil, &character)
+ if err != nil {
+ return res, err
+ }
+
+ res = CharacterResponse{
+ BaseResponse: doRes,
+ Data: character,
+ }
+
+ return res, nil
+}
+
+func (c *Client) RandomCharacter() (CharacterResponse, error) {
+ var character Character
+ var res CharacterResponse
+
+ doRes, err := c.doGetRequest(characterOrigin+"/random", nil, &character)
+ if err != nil {
+ return res, err
+ }
+
+ res = CharacterResponse{
+ BaseResponse: doRes,
+ Data: character,
+ }
+
+ return res, nil
+}
+
+func (c *Client) CreateCharacter(s CreateCharacter) (Character, error) {
+ var character Character
+
+ err := c.doPostRequest(characterOrigin, s, &character)
+ if err != nil {
+ return character, err
+ }
+
+ return character, nil
+}
+
+func (c *Client) PatchCharacter(id uuid.UUID, p PatchCharacter) (Character, error) {
+ var character Character
+
+ err := c.doPatchRequest(characterOrigin+"/"+id.String(), &p)
+ if err != nil {
+ return character, err
+ }
+
+ res, err := c.FindCharacter(id)
+ if err != nil {
+ return character, err
+ }
+
+ character = res.Data
+ return character, nil
+}
+
+func (c *Client) DeleteCharacter(id uuid.UUID) error {
+ return c.doDeleteRequest(characterOrigin + "/" + id.String())
+}
diff --git a/pkg/hawapi/episode.go b/pkg/hawapi/episode.go
new file mode 100644
index 0000000..89cd660
--- /dev/null
+++ b/pkg/hawapi/episode.go
@@ -0,0 +1,136 @@
+package hawapi
+
+import (
+ "github.com/google/uuid"
+)
+
+const episodeOrigin = "episodes"
+
+type Episode struct {
+ Uuid uuid.UUID `json:"uuid"`
+ Href string `json:"href"`
+ Title string `json:"title"`
+ Description string `json:"description"`
+ Language string `json:"language"`
+ Duration int64 `json:"duration"`
+ Season string `json:"season"`
+ EpisodeNum byte `json:"episode_num"`
+ NextEpisode string `json:"next_episode,omitempty"`
+ PrevEpisode string `json:"prev_episode,omitempty"`
+ Thumbnail string `json:"thumbnail"`
+ Images []string `json:"images,omitempty"`
+ Sources []string `json:"sources,omitempty"`
+ CreatedAt string `json:"created_at"`
+ UpdatedAt string `json:"updated_at"`
+}
+
+type CreateEpisode struct {
+ Title string `json:"title"`
+ Description string `json:"description"`
+ Language string `json:"language"`
+ Duration int64 `json:"duration"`
+ Season string `json:"season"`
+ EpisodeNum byte `json:"episode_num"`
+ NextEpisode string `json:"next_episode,omitempty"`
+ PrevEpisode string `json:"prev_episode,omitempty"`
+ Thumbnail string `json:"thumbnail"`
+ Images []string `json:"images,omitempty"`
+ Sources []string `json:"sources,omitempty"`
+}
+
+type PatchEpisode = CreateEpisode
+
+type EpisodeResponse struct {
+ BaseResponse
+ Data Episode `json:"data"`
+}
+
+type EpisodeListResponse struct {
+ BaseResponse
+ Data []Episode `json:"data"`
+}
+
+// ListEpisodes will get all episodes
+func (c *Client) ListEpisodes(options ...QueryOptions) (EpisodeListResponse, error) {
+ var episodes []Episode
+ var res EpisodeListResponse
+
+ doRes, err := c.doGetRequest(episodeOrigin, options, &episodes)
+ if err != nil {
+ return res, err
+ }
+
+ res = EpisodeListResponse{
+ BaseResponse: doRes,
+ Data: episodes,
+ }
+
+ return res, nil
+}
+
+// FindEpisode will get a single item by uuid
+func (c *Client) FindEpisode(id uuid.UUID) (EpisodeResponse, error) {
+ var episode Episode
+ var res EpisodeResponse
+
+ doRes, err := c.doGetRequest(episodeOrigin+"/"+id.String(), nil, &episode)
+ if err != nil {
+ return res, err
+ }
+
+ res = EpisodeResponse{
+ BaseResponse: doRes,
+ Data: episode,
+ }
+
+ return res, nil
+}
+
+func (c *Client) RandomEpisode() (EpisodeResponse, error) {
+ var episode Episode
+ var res EpisodeResponse
+
+ doRes, err := c.doGetRequest(episodeOrigin+"/random", nil, &episode)
+ if err != nil {
+ return res, err
+ }
+
+ res = EpisodeResponse{
+ BaseResponse: doRes,
+ Data: episode,
+ }
+
+ return res, nil
+}
+
+func (c *Client) CreateEpisode(s CreateEpisode) (Episode, error) {
+ var episode Episode
+
+ err := c.doPostRequest(episodeOrigin, s, &episode)
+ if err != nil {
+ return episode, err
+ }
+
+ return episode, nil
+}
+
+func (c *Client) PatchEpisode(id uuid.UUID, p PatchEpisode) (Episode, error) {
+ var episode Episode
+
+ err := c.doPatchRequest(episodeOrigin+"/"+id.String(), &p)
+ if err != nil {
+ return episode, err
+ }
+
+ res, err := c.FindEpisode(id)
+ if err != nil {
+ return episode, err
+ }
+
+ episode = res.Data
+ return episode, nil
+}
+
+func (c *Client) DeleteEpisode(id uuid.UUID) error {
+ return c.doDeleteRequest(episodeOrigin + "/" + id.String())
+}
diff --git a/pkg/hawapi/error.go b/pkg/hawapi/error.go
new file mode 100644
index 0000000..1717ef1
--- /dev/null
+++ b/pkg/hawapi/error.go
@@ -0,0 +1,26 @@
+package hawapi
+
+import "fmt"
+
+type ErrorResponse struct {
+ Code int `json:"code"`
+ Status string `json:"status"`
+ Method string `json:"method"`
+ Cause string `json:"cause"`
+ Url string `json:"url"`
+ Message string `json:"message,omitempty"`
+}
+
+func (e ErrorResponse) Error() string {
+ msg := fmt.Sprintf("request error [%s %d] using %s method", e.Status, e.Code, e.Method)
+
+ if len(e.Url) != 0 {
+ msg = fmt.Sprintf("%s on '%s'", msg, e.Url)
+ }
+
+ if len(e.Message) != 0 {
+ msg = fmt.Sprintf("%s: %s", msg, e.Message)
+ }
+
+ return msg
+}
diff --git a/pkg/hawapi/game.go b/pkg/hawapi/game.go
new file mode 100644
index 0000000..cfe22b0
--- /dev/null
+++ b/pkg/hawapi/game.go
@@ -0,0 +1,150 @@
+package hawapi
+
+import (
+ "github.com/google/uuid"
+)
+
+const gameOrigin = "games"
+
+type Game struct {
+ Uuid string `json:"uuid"`
+ Href string `json:"href"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Playtime int64 `json:"playtime"`
+ Language string `json:"language"`
+ Platforms []string `json:"platforms,omitempty"`
+ Stores []string `json:"stores,omitempty"`
+ Modes []string `json:"modes,omitempty"`
+ Genres []string `json:"genres,omitempty"`
+ Publishers []string `json:"publishers,omitempty"`
+ Developers []string `json:"developers,omitempty"`
+ Website string `json:"website"`
+ Tags []string `json:"tags,omitempty"`
+ Trailer string `json:"trailer"`
+ AgeRating string `json:"age_rating"`
+ ReleaseDate string `json:"release_date"`
+ Thumbnail string `json:"thumbnail"`
+ Images []string `json:"images,omitempty"`
+ Sources []string `json:"sources,omitempty"`
+ CreatedAt string `json:"created_at"`
+ UpdatedAt string `json:"updated_at"`
+}
+
+type CreateGame struct {
+ Name string `json:"name,omitempty"`
+ Description string `json:"description,omitempty"`
+ Playtime int64 `json:"playtime,omitempty"`
+ Language string `json:"language,omitempty"`
+ Platforms []string `json:"platforms,omitempty,omitempty"`
+ Stores []string `json:"stores,omitempty,omitempty"`
+ Modes []string `json:"modes,omitempty,omitempty"`
+ Genres []string `json:"genres,omitempty,omitempty"`
+ Publishers []string `json:"publishers,omitempty,omitempty"`
+ Developers []string `json:"developers,omitempty,omitempty"`
+ Website string `json:"website,omitempty"`
+ Tags []string `json:"tags,omitempty,omitempty"`
+ Trailer string `json:"trailer,omitempty"`
+ AgeRating string `json:"age_rating,omitempty"`
+ ReleaseDate string `json:"release_date,omitempty"`
+ Thumbnail string `json:"thumbnail,omitempty"`
+ Images []string `json:"images,omitempty"`
+ Sources []string `json:"sources,omitempty"`
+}
+
+type PatchGame = CreateGame
+
+type GameResponse struct {
+ BaseResponse
+ Data Game `json:"data"`
+}
+
+type GameListResponse struct {
+ BaseResponse
+ Data []Game `json:"data"`
+}
+
+// ListGames will get all games
+func (c *Client) ListGames(options ...QueryOptions) (GameListResponse, error) {
+ var games []Game
+ var res GameListResponse
+
+ doRes, err := c.doGetRequest(gameOrigin, options, &games)
+ if err != nil {
+ return res, err
+ }
+
+ res = GameListResponse{
+ BaseResponse: doRes,
+ Data: games,
+ }
+
+ return res, nil
+}
+
+// FindGame will get a single item by uuid
+func (c *Client) FindGame(id uuid.UUID) (GameResponse, error) {
+ var game Game
+ var res GameResponse
+
+ doRes, err := c.doGetRequest(gameOrigin+"/"+id.String(), nil, &game)
+ if err != nil {
+ return res, err
+ }
+
+ res = GameResponse{
+ BaseResponse: doRes,
+ Data: game,
+ }
+
+ return res, nil
+}
+
+func (c *Client) RandomGame() (GameResponse, error) {
+ var game Game
+ var res GameResponse
+
+ doRes, err := c.doGetRequest(gameOrigin+"/random", nil, &game)
+ if err != nil {
+ return res, err
+ }
+
+ res = GameResponse{
+ BaseResponse: doRes,
+ Data: game,
+ }
+
+ return res, nil
+}
+
+func (c *Client) CreateGame(s CreateGame) (Game, error) {
+ var game Game
+
+ err := c.doPostRequest(gameOrigin, s, &game)
+ if err != nil {
+ return game, err
+ }
+
+ return game, nil
+}
+
+func (c *Client) PatchGame(id uuid.UUID, p PatchGame) (Game, error) {
+ var game Game
+
+ err := c.doPatchRequest(gameOrigin+"/"+id.String(), &p)
+ if err != nil {
+ return game, err
+ }
+
+ res, err := c.FindGame(id)
+ if err != nil {
+ return game, err
+ }
+
+ game = res.Data
+ return game, nil
+}
+
+func (c *Client) DeleteGame(id uuid.UUID) error {
+ return c.doDeleteRequest(gameOrigin + "/" + id.String())
+}
diff --git a/pkg/hawapi/hawapi.go b/pkg/hawapi/hawapi.go
new file mode 100644
index 0000000..d1fba0e
--- /dev/null
+++ b/pkg/hawapi/hawapi.go
@@ -0,0 +1,166 @@
+package hawapi
+
+import (
+ "log/slog"
+ "net/http"
+ "os"
+ "time"
+
+ "github.com/HawAPI/go-sdk/pkg/cache"
+)
+
+const (
+ DefaultLogLevel = slog.LevelInfo
+ DefaultEndpoint = "https://hawapi.theproject.id/api"
+ DefaultVersion = "v1"
+ DefaultLanguage = "en-US"
+ DefaultSize = 10
+ DefaultTimeout = 10
+ DefaultUseInMemoryCache = true
+)
+
+// DefaultOptions for Go HawAPI SDK
+var DefaultOptions = Options{
+ Endpoint: DefaultEndpoint,
+ Version: DefaultVersion,
+ Language: DefaultLanguage,
+ Size: DefaultSize,
+ Timeout: DefaultTimeout,
+ UseInMemoryCache: DefaultUseInMemoryCache,
+ LogLevel: DefaultLogLevel,
+ LogHandler: nil,
+}
+
+type Options struct {
+ // The endpoint of the HawAPI instance
+ //
+ // Default value: DefaultEndpoint
+ Endpoint string
+
+ // The version of the API
+ Version string
+
+ // The language of items for all requests
+ //
+ // Note: This value can be overwritten later
+ Language string
+
+ // The size of items for all requests
+ //
+ // Note: This value can be overwritten later
+ Size int
+
+ // The timeout of a response in milliseconds
+ Timeout int
+
+ // The HawAPI token (JWT)
+ //
+ // By default, all requests are made with 'ANONYMOUS' tier
+ Token string
+
+ // Define if the package should save (in-memory) all request results
+ UseInMemoryCache bool
+
+ // Define the level of SDK logging
+ //
+ // NOTE: If you are using a custom LogHandler, use slog.HandlerOptions to define a new log level or the SDK will panic
+ LogLevel slog.Level
+
+ // Defines the log handler.
+ //
+ // If set to nil, it defaults to NewFormattedHandler
+ LogHandler slog.Handler
+}
+
+// Client is the [HawAPI] golang client.
+//
+// - [GitHub]
+// - [Examples]
+//
+// [HawAPI]: https://github.com/HawAPI/HawAPI
+// [GitHub]: https://github.com/HawAPI/go-sdk/
+// [Examples]: https://github.com/HawAPI/go-sdk/examples/
+type Client struct {
+ options Options
+ client *http.Client
+ logger *slog.Logger
+ cache cache.Cache
+}
+
+// NewClient creates a new HawAPI client using the default options.
+func NewClient() Client {
+ c := Client{options: DefaultOptions}
+
+ c.client = &http.Client{
+ Timeout: time.Duration(c.options.Timeout) * time.Second,
+ }
+
+ c.logger = slog.New(NewFormattedHandler(os.Stdout, &slog.HandlerOptions{
+ Level: c.options.LogLevel,
+ }))
+
+ c.cache = cache.NewMemoryCache()
+ return c
+}
+
+// NewClientWithOpts creates a new HawAPI client using custom options.
+func NewClientWithOpts(options Options) Client {
+ c := NewClient()
+ c.WithOpts(options)
+ return c
+}
+
+// WithOpts will set or override current client options
+func (c *Client) WithOpts(options Options) {
+ if len(options.Endpoint) != 0 {
+ c.options.Endpoint = options.Endpoint
+ }
+
+ if len(options.Version) != 0 {
+ c.options.Version = options.Version
+ }
+
+ if len(options.Language) != 0 {
+ c.options.Language = options.Language
+ }
+
+ if options.Size != 0 {
+ c.options.Size = options.Size
+ }
+
+ if options.Timeout != 0 {
+ c.options.Timeout = options.Timeout
+ }
+
+ if options.LogLevel != DefaultLogLevel {
+ c.options.LogLevel = options.LogLevel
+
+ if options.LogHandler != nil {
+ panic("when defining log handler, use slog.HandlerOptions instead")
+ }
+
+ c.logger = slog.New(NewFormattedHandler(os.Stdout, &slog.HandlerOptions{
+ Level: options.LogLevel,
+ }))
+ }
+
+ if options.LogHandler != nil {
+ c.logger = slog.New(options.LogHandler)
+ }
+
+ if !options.UseInMemoryCache {
+ c.logger.Warn("Using WithOpts method, the value of UseInMemoryCache will be set to false")
+ }
+
+ c.options.UseInMemoryCache = options.UseInMemoryCache
+}
+
+// ClearCache deletes all values from the cache and returns the count of deleted items
+func (c *Client) ClearCache() int {
+ return c.cache.Clear()
+}
+
+// CacheSize returns the count cache items
+func (c *Client) CacheSize() int {
+ return c.cache.Size()
+}
diff --git a/pkg/hawapi/info.go b/pkg/hawapi/info.go
new file mode 100644
index 0000000..59333a4
--- /dev/null
+++ b/pkg/hawapi/info.go
@@ -0,0 +1,35 @@
+package hawapi
+
+import "net/http"
+
+type Info struct {
+ Title string `json:"title"`
+ Description string `json:"description"`
+ Version string `json:"version"`
+ Url string `json:"url"`
+ Docs string `json:"docs"`
+ Github string `json:"github"`
+ License string `json:"license"`
+ GithubHome string `json:"github_home"`
+ ApiUrl string `json:"api_url"`
+ ApiVersion string `json:"api_version"`
+ ApiPath string `json:"api_path"`
+ ApiBaseUrl string `json:"api_base_url"`
+ LicenseUrl string `json:"license_url"`
+}
+
+func (c *Client) Info() (Info, error) {
+ var info Info
+
+ req, err := http.NewRequest(http.MethodGet, c.options.Endpoint, nil)
+ if err != nil {
+ return info, err
+ }
+
+ _, err = c.doRequest(req, http.StatusOK, &info)
+ if err != nil {
+ return info, err
+ }
+
+ return info, nil
+}
diff --git a/pkg/hawapi/location.go b/pkg/hawapi/location.go
new file mode 100644
index 0000000..6831b4f
--- /dev/null
+++ b/pkg/hawapi/location.go
@@ -0,0 +1,126 @@
+package hawapi
+
+import (
+ "github.com/google/uuid"
+)
+
+const locationOrigin = "locations"
+
+type Location struct {
+ Uuid uuid.UUID `json:"uuid"`
+ Href string `json:"href"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Language string `json:"language"`
+ Thumbnail string `json:"thumbnail,omitempty"`
+ Images []string `json:"images,omitempty"`
+ Sources []string `json:"sources,omitempty"`
+ CreatedAt string `json:"created_at"`
+ UpdatedAt string `json:"updated_at"`
+}
+
+type CreateLocation struct {
+ Name string `json:"name,omitempty"`
+ Description string `json:"description,omitempty"`
+ Language string `json:"language,omitempty"`
+ Thumbnail string `json:"thumbnail,omitempty"`
+ Images []string `json:"images,omitempty"`
+ Sources []string `json:"sources,omitempty"`
+}
+
+type PatchLocation = CreateLocation
+
+type LocationResponse struct {
+ BaseResponse
+ Data Location `json:"data"`
+}
+
+type LocationListResponse struct {
+ BaseResponse
+ Data []Location `json:"data"`
+}
+
+// ListLocations will get all locations
+func (c *Client) ListLocations(options ...QueryOptions) (LocationListResponse, error) {
+ var locations []Location
+ var res LocationListResponse
+
+ doRes, err := c.doGetRequest(locationOrigin, options, &locations)
+ if err != nil {
+ return res, err
+ }
+
+ res = LocationListResponse{
+ BaseResponse: doRes,
+ Data: locations,
+ }
+
+ return res, nil
+}
+
+// FindLocation will get a single item by uuid
+func (c *Client) FindLocation(id uuid.UUID) (LocationResponse, error) {
+ var location Location
+ var res LocationResponse
+
+ doRes, err := c.doGetRequest(locationOrigin+"/"+id.String(), nil, &location)
+ if err != nil {
+ return res, err
+ }
+
+ res = LocationResponse{
+ BaseResponse: doRes,
+ Data: location,
+ }
+
+ return res, nil
+}
+
+func (c *Client) RandomLocation() (LocationResponse, error) {
+ var location Location
+ var res LocationResponse
+
+ doRes, err := c.doGetRequest(locationOrigin+"/random", nil, &location)
+ if err != nil {
+ return res, err
+ }
+
+ res = LocationResponse{
+ BaseResponse: doRes,
+ Data: location,
+ }
+
+ return res, nil
+}
+
+func (c *Client) CreateLocation(s CreateLocation) (Location, error) {
+ var location Location
+
+ err := c.doPostRequest(locationOrigin, s, &location)
+ if err != nil {
+ return location, err
+ }
+
+ return location, nil
+}
+
+func (c *Client) PatchLocation(id uuid.UUID, p PatchLocation) (Location, error) {
+ var location Location
+
+ err := c.doPatchRequest(locationOrigin+"/"+id.String(), &p)
+ if err != nil {
+ return location, err
+ }
+
+ res, err := c.FindLocation(id)
+ if err != nil {
+ return location, err
+ }
+
+ location = res.Data
+ return location, nil
+}
+
+func (c *Client) DeleteLocation(id uuid.UUID) error {
+ return c.doDeleteRequest(locationOrigin + "/" + id.String())
+}
diff --git a/pkg/hawapi/logger.go b/pkg/hawapi/logger.go
new file mode 100644
index 0000000..15e2a35
--- /dev/null
+++ b/pkg/hawapi/logger.go
@@ -0,0 +1,70 @@
+package hawapi
+
+import (
+ "context"
+ "encoding/json"
+ "io"
+ "log"
+ "log/slog"
+
+ "github.com/fatih/color"
+)
+
+type FormatterHandler struct {
+ slog.Handler
+ logger *log.Logger
+ attrs []slog.Attr
+}
+
+func NewFormattedHandler(out io.Writer, opts *slog.HandlerOptions) *FormatterHandler {
+ return &FormatterHandler{
+ Handler: slog.NewJSONHandler(out, opts),
+ logger: log.New(out, "", 0),
+ }
+}
+
+func (h *FormatterHandler) Handle(_ context.Context, r slog.Record) error {
+ fields := make(map[string]interface{}, r.NumAttrs())
+
+ r.Attrs(func(a slog.Attr) bool {
+ fields[a.Key] = a.Value.Any()
+ return true
+ })
+
+ for _, a := range h.attrs {
+ fields[a.Key] = a.Value.Any()
+ }
+
+ var b []byte
+ var err error
+ if len(fields) > 0 {
+ b, err = json.MarshalIndent(fields, "", " ")
+ if err != nil {
+ return err
+ }
+ }
+
+ timeStr := r.Time.Local().Format("2006/01/02 15:04:05")
+ msg := r.Message
+
+ level := r.Level.String() + ":"
+ switch r.Level {
+ case slog.LevelDebug:
+ level = color.BlueString(level)
+ case slog.LevelInfo:
+ level = color.GreenString(level)
+ case slog.LevelWarn:
+ level = color.YellowString(level)
+ case slog.LevelError:
+ level = color.RedString(level)
+ }
+
+ h.logger.Println(
+ timeStr,
+ level,
+ msg,
+ color.WhiteString(string(b)),
+ )
+
+ return nil
+}
diff --git a/pkg/hawapi/overview.go b/pkg/hawapi/overview.go
new file mode 100644
index 0000000..4cc456f
--- /dev/null
+++ b/pkg/hawapi/overview.go
@@ -0,0 +1,37 @@
+package hawapi
+
+type DataCount struct {
+ Actors int `json:"actors"`
+ Characters int `json:"characters"`
+ Episodes int `json:"episodes"`
+ Games int `json:"games"`
+ Locations int `json:"locations"`
+ Seasons int `json:"seasons"`
+ Soundtracks int `json:"soundtracks"`
+}
+
+type Overview struct {
+ Uuid string `json:"uuid"`
+ Href string `json:"href"`
+ Sources []string `json:"sources"`
+ Thumbnail string `json:"thumbnail"`
+ Title string `json:"title"`
+ Description string `json:"description"`
+ Language string `json:"language"`
+ Languages []string `json:"languages"`
+ Creators []string `json:"creators"`
+ CreatedAt string `json:"created_at"`
+ UpdatedAt string `json:"updated_at"`
+ DataCount DataCount `json:"data_count"`
+}
+
+func (c *Client) Overview(options ...QueryOptions) (Overview, error) {
+ var overview Overview
+
+ _, err := c.doGetRequest("overview", options, &overview)
+ if err != nil {
+ return overview, err
+ }
+
+ return overview, nil
+}
diff --git a/pkg/hawapi/pageable.go b/pkg/hawapi/pageable.go
new file mode 100644
index 0000000..6b503e4
--- /dev/null
+++ b/pkg/hawapi/pageable.go
@@ -0,0 +1,15 @@
+package hawapi
+
+type Pageable struct {
+ Page int `json:"page"`
+ Size int `json:"size"`
+ Sort string `json:"sort"`
+ Order string `json:"order"`
+}
+
+var DefaultPageable = Pageable{
+ Page: 1,
+ Size: DefaultSize,
+ Sort: "",
+ Order: "ASC",
+}
diff --git a/pkg/hawapi/query_options.go b/pkg/hawapi/query_options.go
new file mode 100644
index 0000000..69036fb
--- /dev/null
+++ b/pkg/hawapi/query_options.go
@@ -0,0 +1,80 @@
+package hawapi
+
+type Filters map[string]string
+
+type queryOptions struct {
+ Pageable
+ Filters
+}
+
+type QueryOptions func(*queryOptions)
+
+func NewQueryOptions(pageable Pageable, filters Filters) QueryOptions {
+ return func(o *queryOptions) {
+ o.Pageable = pageable
+ o.Filters = filters
+ }
+}
+
+// newQueryOptions wil create a new queryOptions with default and pre-defined values.
+//
+// Values like 'page size' and 'language' are configured on Client initialization
+func (c *Client) newQueryOptions() queryOptions {
+ opts := queryOptions{
+ Pageable: Pageable{
+ Page: 1,
+ Size: DefaultSize,
+ Sort: "",
+ Order: "ASC",
+ },
+ Filters: make(Filters),
+ }
+
+ return opts
+}
+
+func WithFilters(filters Filters) QueryOptions {
+ return func(o *queryOptions) {
+ o.Filters = filters
+ }
+}
+
+func WithFilter(key string, value string) QueryOptions {
+ return func(o *queryOptions) {
+ o.Filters[key] = value
+ }
+}
+
+func WithLanguage(language string) QueryOptions {
+ return WithFilter("language", language)
+}
+
+func WithPageable(pageable Pageable) QueryOptions {
+ return func(o *queryOptions) {
+ o.Pageable = pageable
+ }
+}
+
+func WithPage(page int) QueryOptions {
+ return func(o *queryOptions) {
+ o.Page = page
+ }
+}
+
+func WithSize(size int) QueryOptions {
+ return func(o *queryOptions) {
+ o.Size = size
+ }
+}
+
+func WithSort(sort string) QueryOptions {
+ return func(o *queryOptions) {
+ o.Sort = sort
+ }
+}
+
+func WithOrder(order string) QueryOptions {
+ return func(o *queryOptions) {
+ o.Order = order
+ }
+}
diff --git a/pkg/hawapi/response.go b/pkg/hawapi/response.go
new file mode 100644
index 0000000..2242485
--- /dev/null
+++ b/pkg/hawapi/response.go
@@ -0,0 +1,27 @@
+package hawapi
+
+// Quota represents the quota status
+type Quota struct {
+ Remaining int `json:"remaining,omitempty"`
+}
+
+// HeaderResponse represents the formatted header response from a request
+type HeaderResponse struct {
+ Page int `json:"page,omitempty"`
+ PageSize int `json:"page_size,omitempty"`
+ PageTotal int `json:"page_total,omitempty"`
+ ItemSize int `json:"item_size,omitempty"`
+ NextPage int `json:"next_page,omitempty"`
+ PrevPage int `json:"prev_page,omitempty"`
+ Language string `json:"language,omitempty"`
+ Quota Quota `json:"quota"`
+ Etag string `json:"etag"`
+ Length int `json:"length"`
+}
+
+// BaseResponse represents all required response fields
+type BaseResponse struct {
+ HeaderResponse
+ Cached bool `json:"cached,omitempty"`
+ Status int `json:"status"`
+}
diff --git a/pkg/hawapi/season.go b/pkg/hawapi/season.go
new file mode 100644
index 0000000..2f29c87
--- /dev/null
+++ b/pkg/hawapi/season.go
@@ -0,0 +1,144 @@
+package hawapi
+
+import (
+ "github.com/google/uuid"
+)
+
+const seasonOrigin = "seasons"
+
+type Season struct {
+ Uuid uuid.UUID `json:"uuid"`
+ Href string `json:"href"`
+ Title string `json:"title"`
+ Description string `json:"description"`
+ Language string `json:"language"`
+ Genres []string `json:"genres,omitempty"`
+ Episodes []string `json:"episodes,omitempty"`
+ Trailers []string `json:"trailers,omitempty"`
+ Budget int `json:"budget"`
+ DurationTotal int64 `json:"duration_total"`
+ SeasonNum byte `json:"season_num"`
+ ReleaseDate string `json:"release_date"`
+ NextSeason string `json:"next_season,omitempty"`
+ PrevSeason string `json:"prev_season,omitempty"`
+ Thumbnail string `json:"thumbnail,omitempty"`
+ Images []string `json:"images,omitempty"`
+ Sources []string `json:"sources,omitempty"`
+ CreatedAt string `json:"created_at"`
+ UpdatedAt string `json:"updated_at"`
+}
+
+type CreateSeason struct {
+ Title string `json:"title,omitempty"`
+ Description string `json:"description,omitempty"`
+ Language string `json:"language,omitempty"`
+ Genres []string `json:"genres,omitempty"`
+ Episodes []string `json:"episodes,omitempty"`
+ Trailers []string `json:"trailers,omitempty"`
+ Budget int `json:"budget,omitempty"`
+ DurationTotal int64 `json:"duration_total,omitempty"`
+ SeasonNum byte `json:"season_num,omitempty"`
+ ReleaseDate string `json:"release_date,omitempty"`
+ NextSeason string `json:"next_season,omitempty"`
+ PrevSeason string `json:"prev_season,omitempty"`
+ Thumbnail string `json:"thumbnail,omitempty"`
+ Images []string `json:"images,omitempty"`
+ Sources []string `json:"sources,omitempty"`
+}
+
+type PatchSeason = CreateSeason
+
+type SeasonResponse struct {
+ BaseResponse
+ Data Season `json:"data"`
+}
+
+type SeasonListResponse struct {
+ BaseResponse
+ Data []Season `json:"data"`
+}
+
+// ListSeasons will get all seasons
+func (c *Client) ListSeasons(options ...QueryOptions) (SeasonListResponse, error) {
+ var seasons []Season
+ var res SeasonListResponse
+
+ doRes, err := c.doGetRequest(seasonOrigin, options, &seasons)
+ if err != nil {
+ return res, err
+ }
+
+ res = SeasonListResponse{
+ BaseResponse: doRes,
+ Data: seasons,
+ }
+
+ return res, nil
+}
+
+// FindSeason will get a single item by uuid
+func (c *Client) FindSeason(id uuid.UUID) (SeasonResponse, error) {
+ var season Season
+ var res SeasonResponse
+
+ doRes, err := c.doGetRequest(seasonOrigin+"/"+id.String(), nil, &season)
+ if err != nil {
+ return res, err
+ }
+
+ res = SeasonResponse{
+ BaseResponse: doRes,
+ Data: season,
+ }
+
+ return res, nil
+}
+
+func (c *Client) RandomSeason() (SeasonResponse, error) {
+ var season Season
+ var res SeasonResponse
+
+ doRes, err := c.doGetRequest(seasonOrigin+"/random", nil, &season)
+ if err != nil {
+ return res, err
+ }
+
+ res = SeasonResponse{
+ BaseResponse: doRes,
+ Data: season,
+ }
+
+ return res, nil
+}
+
+func (c *Client) CreateSeason(s CreateSeason) (Season, error) {
+ var season Season
+
+ err := c.doPostRequest(seasonOrigin, s, &season)
+ if err != nil {
+ return season, err
+ }
+
+ return season, nil
+}
+
+func (c *Client) PatchSeason(id uuid.UUID, p PatchSeason) (Season, error) {
+ var season Season
+
+ err := c.doPatchRequest(seasonOrigin+"/"+id.String(), &p)
+ if err != nil {
+ return season, err
+ }
+
+ res, err := c.FindSeason(id)
+ if err != nil {
+ return season, err
+ }
+
+ season = res.Data
+ return season, nil
+}
+
+func (c *Client) DeleteSeason(id uuid.UUID) error {
+ return c.doDeleteRequest(seasonOrigin + "/" + id.String())
+}
diff --git a/pkg/hawapi/service.go b/pkg/hawapi/service.go
new file mode 100644
index 0000000..689a72b
--- /dev/null
+++ b/pkg/hawapi/service.go
@@ -0,0 +1,358 @@
+package hawapi
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "reflect"
+ "strconv"
+ "strings"
+)
+
+type cachedBaseResponse struct {
+ BaseResponse
+ data []byte
+}
+
+const (
+ // ApiHeaderRateLimitRemaining is the API rate limit remaining
+ apiHeaderRateLimitRemaining = "X-Rate-Limit-Remaining"
+
+ // ApiHeaderPageIndex is the API page index header
+ apiHeaderPageIndex = "X-Pagination-Page-Index"
+
+ // ApiHeaderPageSize is the API page size header
+ apiHeaderPageSize = "X-Pagination-Page-Size"
+
+ // ApiHeaderPageTotal is the API page total header
+ apiHeaderPageTotal = "X-Pagination-Page-Total"
+
+ // ApiHeaderItemTotal is the API item total header
+ apiHeaderItemTotal = "X-Pagination-Item-Total"
+
+ // ApiHeaderContentLanguage is the API language header
+ apiHeaderContentLanguage = "Content-Language"
+
+ // ApiHeaderContentLength is the API content length
+ apiHeaderContentLength = "Content-Length"
+
+ // ApiHeaderEtag is the API content etag
+ apiHeaderEtag = "ETag"
+)
+
+func (c *Client) doRequest(req *http.Request, wantStatus int, out any) (http.Header, error) {
+ if r := reflect.ValueOf(out); out != nil && r.Kind() != reflect.Ptr {
+ return nil, fmt.Errorf("out must be a pointer")
+ }
+
+ req.Header.Set("Content-Type", "application/json")
+
+ // Token is optional
+ if len(c.options.Token) != 0 {
+ req.Header.Set("Authorization", "Bearer "+c.options.Token)
+ }
+
+ res, err := c.client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer res.Body.Close()
+
+ body, err := io.ReadAll(res.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ if res.StatusCode != wantStatus {
+ var resErr ErrorResponse
+ if err := json.Unmarshal(body, &resErr); err != nil {
+ return nil, errors.New("failed to parse error message: " + err.Error())
+ }
+ return nil, resErr
+ }
+
+ if out != nil {
+ if err := json.Unmarshal(body, out); err != nil {
+ return nil, err
+ }
+ }
+
+ return res.Header, nil
+}
+
+func (c *Client) doGetRequest(origin string, query []QueryOptions, out any) (BaseResponse, error) {
+ var res BaseResponse
+
+ // This will fix 'buildUrl' ignoring url options if 'query' is nil
+ if query == nil {
+ query = []QueryOptions{}
+ }
+
+ url := c.buildUrl(origin, query)
+
+ cached, ok := c.cache.Get(url)
+ if ok {
+ cbr := cached.(cachedBaseResponse)
+
+ // If the cache doesn't work, we fetch the data again
+ if err := json.Unmarshal(cbr.data, out); err == nil {
+ c.logger.Debug(fmt.Sprintf("found cached response for key %s", url))
+ return cbr.BaseResponse, nil
+ }
+
+ c.logger.Warn("failed to parse response from in-memory cache, fetching...")
+ }
+
+ req, err := http.NewRequest(http.MethodGet, url, nil)
+ if err != nil {
+ return res, err
+ }
+
+ httpHeader, err := c.doRequest(req, http.StatusOK, out)
+ if err != nil {
+ return res, err
+ }
+
+ headers := c.extractHeaders(httpHeader)
+ res = BaseResponse{
+ HeaderResponse: headers,
+ Status: http.StatusOK,
+ }
+
+ if c.options.UseInMemoryCache {
+ res.Cached = true
+
+ bOut, err := json.Marshal(out)
+ if err != nil {
+ return res, err
+ }
+
+ cbr := cachedBaseResponse{
+ BaseResponse: res,
+ data: bOut,
+ }
+
+ c.logger.Debug(fmt.Sprintf("cached response using '%s' as key", url))
+ c.cache.Set(url, cbr)
+ }
+
+ return res, nil
+}
+
+func (c *Client) doPostRequest(origin string, in any, out any) error {
+ if len(c.options.Token) == 0 {
+ return fmt.Errorf("token is required for post request")
+ }
+
+ url := c.buildUrl(origin, nil)
+ body, err := json.Marshal(in)
+ if err != nil {
+ return err
+ }
+
+ req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(body))
+ if err != nil {
+ return err
+ }
+
+ _, err = c.doRequest(req, http.StatusCreated, out)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (c *Client) doPatchRequest(origin string, patch any) error {
+ if len(c.options.Token) == 0 {
+ return fmt.Errorf("token is required for put request")
+ }
+
+ var item any
+ _, err := c.doGetRequest(origin, nil, &item)
+ if err != nil {
+ return err
+ }
+
+ res, err := json.Marshal(patch)
+ if err != nil {
+ return err
+ }
+
+ err = json.Unmarshal(res, &item)
+ if err != nil {
+ return err
+ }
+
+ itemBytes, err := json.Marshal(item)
+ if err != nil {
+ return err
+ }
+
+ url := c.buildUrl(origin, nil)
+ req, err := http.NewRequest(http.MethodPatch, url, bytes.NewBuffer(itemBytes))
+ if err != nil {
+ return err
+ }
+
+ _, err = c.doRequest(req, http.StatusOK, nil)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (c *Client) doDeleteRequest(origin string) error {
+ if len(c.options.Token) == 0 {
+ return fmt.Errorf("token is required for delete request")
+ }
+
+ url := c.buildUrl(origin, nil)
+ req, err := http.NewRequest(http.MethodDelete, url, nil)
+ if err != nil {
+ return err
+ }
+
+ _, err = c.doRequest(req, http.StatusNoContent, nil)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (c *Client) buildUrl(origin string, query []QueryOptions) string {
+ url := fmt.Sprintf("%s/%s/%s", c.options.Endpoint, c.options.Version, origin)
+
+ // No options to append
+ if query == nil {
+ c.logger.Debug("building url without query options")
+ return url
+ }
+
+ var params []string
+
+ // Don't set language param if it's the same as default
+ if len(c.options.Language) != 0 && c.options.Language != DefaultLanguage {
+ params = pushOrOverwrite(params, "language", c.options.Language)
+ }
+
+ // Don't set size param if it's the same as default
+ if c.options.Size != 0 && c.options.Size != DefaultSize {
+ params = pushOrOverwrite(params, "size", strconv.Itoa(c.options.Size))
+ }
+
+ opts := c.newQueryOptions()
+ for _, opt := range query {
+ opt(&opts)
+ }
+
+ for key, value := range opts.Filters {
+ if value != "" {
+ params = pushOrOverwrite(params, key, value)
+ }
+ }
+
+ if opts.Pageable.Page != 0 && opts.Pageable.Page != 1 {
+ params = pushOrOverwrite(params, "page", strconv.Itoa(opts.Pageable.Page))
+ }
+
+ if opts.Pageable.Size != 0 && opts.Pageable.Size != DefaultSize {
+ params = pushOrOverwrite(params, "size", strconv.Itoa(opts.Pageable.Size))
+ }
+
+ if opts.Pageable.Sort != "" {
+ sortParam := opts.Pageable.Sort
+ if opts.Pageable.Order != "" {
+ sortParam = fmt.Sprintf("%s,%s", sortParam, opts.Pageable.Order)
+ }
+ params = pushOrOverwrite(params, "sort", sortParam)
+ }
+
+ paramsStr := ""
+ if len(params) > 0 {
+ paramsStr = "?" + strings.Join(params, "&")
+ }
+
+ url += paramsStr
+ c.logger.Debug("final url: " + url)
+ return url
+}
+
+func pushOrOverwrite(params []string, key, value string) []string {
+ for i, param := range params {
+ if strings.HasPrefix(param, key+"=") {
+ params[i] = fmt.Sprintf("%s=%s", key, value)
+ return params
+ }
+ }
+ return append(params, fmt.Sprintf("%s=%s", key, value))
+}
+
+func (c *Client) extractHeaders(header http.Header) HeaderResponse {
+ var headers HeaderResponse
+
+ rateLimitRemaining := header.Get(apiHeaderRateLimitRemaining)
+ headers.Quota.Remaining = c.parseInt(rateLimitRemaining)
+
+ pageStr := header.Get(apiHeaderPageIndex)
+ headers.Page = c.parseInt(pageStr)
+
+ pageSizeStr := header.Get(apiHeaderPageSize)
+ headers.PageSize = c.parseInt(pageSizeStr)
+
+ pageTotalStr := header.Get(apiHeaderPageTotal)
+ headers.PageTotal = c.parseInt(pageTotalStr)
+
+ itemStr := header.Get(apiHeaderItemTotal)
+ headers.ItemSize = c.parseInt(itemStr)
+
+ lengthStr := header.Get(apiHeaderContentLength)
+ headers.Length = c.parseInt(lengthStr)
+
+ nextPage := c.handlePagination(headers.Page, true)
+ headers.NextPage = nextPage
+
+ prevPage := c.handlePagination(headers.Page, false)
+ headers.PrevPage = prevPage
+
+ headers.Etag = header.Get(apiHeaderEtag)
+ headers.Language = header.Get(apiHeaderContentLanguage)
+ return headers
+}
+
+func (c *Client) handlePagination(page int, increase bool) int {
+ if page <= 0 {
+ return -1
+ }
+
+ if increase {
+ page++
+ } else {
+ page--
+ }
+
+ if page == 0 {
+ return -1
+ }
+
+ return page
+}
+
+func (c *Client) parseInt(s string) int {
+ if len(s) == 0 {
+ return -1
+ }
+
+ i, err := strconv.Atoi(s)
+ if err != nil {
+ c.logger.Debug(fmt.Sprintf("failed to parse integer: %s", s))
+ return -1
+ }
+
+ return i
+}
diff --git a/pkg/hawapi/service_test.go b/pkg/hawapi/service_test.go
new file mode 100644
index 0000000..67dbc4e
--- /dev/null
+++ b/pkg/hawapi/service_test.go
@@ -0,0 +1,403 @@
+package hawapi
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "reflect"
+ "testing"
+
+ "github.com/HawAPI/go-sdk/pkg/cache"
+)
+
+func TestClient_buildUrl(t *testing.T) {
+ type fields struct {
+ options Options
+ }
+ type args struct {
+ origin string
+ query []QueryOptions
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ want string
+ }{
+ {
+ name: "should build a simple url",
+ fields: fields{},
+ args: args{
+ origin: "actors",
+ },
+ want: "https://hawapi.theproject.id/api/v1/actors",
+ },
+ {
+ name: "should build url with custom endpoint and version",
+ fields: fields{
+ options: Options{
+ Endpoint: "https://hawapi.example.id/api",
+ Version: "v3",
+ },
+ },
+ args: args{
+ origin: "actors",
+ },
+ want: "https://hawapi.example.id/api/v3/actors",
+ },
+ {
+ name: "should build url with global options",
+ fields: fields{
+ options: Options{
+ Language: "pt-BR",
+ Size: 20,
+ },
+ },
+ args: args{
+ origin: "actors",
+ query: []QueryOptions{},
+ },
+ want: "https://hawapi.theproject.id/api/v1/actors?language=pt-BR&size=20",
+ },
+ {
+ name: "should build url with pageable",
+ fields: fields{},
+ args: args{
+ origin: "actors",
+ query: []QueryOptions{
+ WithPage(2),
+ WithSize(40),
+ },
+ },
+ want: "https://hawapi.theproject.id/api/v1/actors?page=2&size=40",
+ },
+ {
+ name: "should build url with sort",
+ fields: fields{},
+ args: args{
+ origin: "actors",
+ query: []QueryOptions{
+ WithSort("first_name"),
+ WithOrder("DESC"),
+ },
+ },
+ want: "https://hawapi.theproject.id/api/v1/actors?sort=first_name,DESC",
+ },
+ {
+ name: "should build ignore order if sort is not present",
+ fields: fields{},
+ args: args{
+ origin: "actors",
+ query: []QueryOptions{
+ WithOrder("DESC"),
+ },
+ },
+ want: "https://hawapi.theproject.id/api/v1/actors",
+ },
+ {
+ name: "should build overwrite filter if is already set",
+ fields: fields{},
+ args: args{
+ origin: "actors",
+ query: []QueryOptions{
+ WithFilter("gender", "1"),
+ WithFilter("first_name", "Finn"),
+ WithFilter("gender", "0"),
+ },
+ },
+ want: "https://hawapi.theproject.id/api/v1/actors?gender=0&first_name=Finn",
+ },
+ {
+ name: "should build a complete url",
+ fields: fields{},
+ args: args{
+ origin: "actors",
+ query: []QueryOptions{
+ WithLanguage("fr-FR"),
+ WithSize(20),
+ WithFilter("gender", "1"),
+ WithSort("first_name"),
+ WithOrder("DESC"),
+ },
+ },
+ want: "https://hawapi.theproject.id/api/v1/actors?language=fr-FR&gender=1&size=20&sort=first_name,DESC",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := NewClientWithOpts(tt.fields.options)
+
+ if got := c.buildUrl(tt.args.origin, tt.args.query); got != tt.want {
+ t.Errorf("buildUrl() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func Test_extractHeaders(t *testing.T) {
+ type args struct {
+ header http.Header
+ }
+ tests := []struct {
+ name string
+ args args
+ want HeaderResponse
+ }{
+ {
+ name: "test",
+ args: args{
+ header: http.Header{
+ apiHeaderRateLimitRemaining: []string{"15"},
+ apiHeaderContentLanguage: []string{"fr-FR"},
+ apiHeaderContentLength: []string{"123"},
+ apiHeaderItemTotal: []string{"10"},
+ apiHeaderPageIndex: []string{"1"},
+ apiHeaderPageSize: []string{"10"},
+ apiHeaderPageTotal: []string{"1"},
+ },
+ },
+ want: HeaderResponse{
+ Quota: Quota{Remaining: 15},
+ Language: "fr-FR",
+ Length: 123,
+ ItemSize: 10,
+ Page: 1,
+ PageSize: 10,
+ PageTotal: 1,
+ NextPage: 2,
+ PrevPage: -1,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := extractHeaders(tt.args.header); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("extractHeaders() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestClient_doRequest(t *testing.T) {
+ type fields struct {
+ options Options
+ }
+ type args struct {
+ reqMethod string
+ mockStatus int
+ wantStatus int
+ out any
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ wantErr bool
+ }{
+ {
+ name: "should do request successfully",
+ fields: fields{},
+ args: args{
+ reqMethod: "GET",
+ mockStatus: http.StatusOK,
+ wantStatus: http.StatusOK,
+ out: nil,
+ },
+ wantErr: false,
+ },
+ {
+ name: "should return error if status is not as expected",
+ fields: fields{},
+ args: args{
+ reqMethod: "GET",
+ mockStatus: http.StatusInternalServerError,
+ wantStatus: http.StatusOK,
+ },
+ wantErr: true,
+ },
+ {
+ name: "should return error if out is not a pointer",
+ fields: fields{},
+ args: args{
+ reqMethod: "GET",
+ out: Actor{},
+ },
+ wantErr: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := NewClientWithOpts(tt.fields.options)
+
+ sv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(tt.args.mockStatus)
+ }))
+ defer sv.Close()
+
+ req, err := http.NewRequest(tt.args.reqMethod, sv.URL, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = c.doRequest(req, tt.args.wantStatus, tt.args.out)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("doRequest() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ })
+ }
+}
+
+func Test_handlePagination(t *testing.T) {
+ type args struct {
+ page int
+ increase bool
+ }
+ tests := []struct {
+ name string
+ args args
+ want int
+ }{
+ {
+ name: "should return -1 with page value being 0 (decrease)",
+ args: args{
+ page: 0,
+ increase: false,
+ },
+ want: -1,
+ },
+ {
+ name: "should return -1 with page value being 0 (increase)",
+ args: args{
+ page: 0,
+ increase: true,
+ },
+ want: -1,
+ },
+ {
+ name: "should return -1 with page value being -1 (decrease)",
+ args: args{
+ page: -1,
+ increase: false,
+ },
+ want: -1,
+ },
+ {
+ name: "should return 1 with page value being 2 (decrease)",
+ args: args{
+ page: 2,
+ increase: false,
+ },
+ want: 1,
+ },
+ {
+ name: "should return -1 with page value being 1 (decrease)",
+ args: args{
+ page: 1,
+ increase: false,
+ },
+ want: -1,
+ },
+ {
+ name: "should return 2 with page value being 1 (increase)",
+ args: args{
+ page: 1,
+ increase: true,
+ },
+ want: 2,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := handlePagination(tt.args.page, tt.args.increase); got != tt.want {
+ t.Errorf("handlePagination() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestClient_doGetRequest(t *testing.T) {
+ type fields struct {
+ options Options
+ cache cache.Cache
+ }
+ type args struct {
+ origin string
+ query []QueryOptions
+ out any
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ want BaseResponse
+ wantErr bool
+ }{
+ {
+ name: "should call get request successfully",
+ fields: fields{
+ options: DefaultOptions,
+ cache: cache.NewMemoryCache(),
+ },
+ args: args{
+ origin: "actors",
+ out: &Actor{},
+ },
+ want: BaseResponse{
+ HeaderResponse: HeaderResponse{
+ Page: -1,
+ PageSize: -1,
+ PageTotal: -1,
+ ItemSize: -1,
+ NextPage: -1,
+ PrevPage: -1,
+ Language: "",
+ Quota: Quota{
+ Remaining: -1,
+ },
+ Etag: "",
+ Length: 45,
+ },
+ Cached: true,
+ Status: 200,
+ },
+ wantErr: false,
+ },
+ {
+ name: "should return error if out is not a pointer",
+ fields: fields{
+ options: DefaultOptions,
+ cache: cache.NewMemoryCache(),
+ },
+ args: args{
+ origin: "actors",
+ out: Actor{},
+ },
+ wantErr: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+ w.Write([]byte(`{"first_name": "Lorem", "last_name": "Ipsum"}`))
+ }))
+ defer server.Close()
+
+ tt.fields.options.Endpoint = server.URL
+ c := &Client{
+ options: tt.fields.options,
+ client: server.Client(),
+ cache: tt.fields.cache,
+ }
+
+ got, err := c.doGetRequest(tt.args.origin, tt.args.query, tt.args.out)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("doGetRequest() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("doGetRequest() got = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/pkg/hawapi/soundtrack.go b/pkg/hawapi/soundtrack.go
new file mode 100644
index 0000000..f24826e
--- /dev/null
+++ b/pkg/hawapi/soundtrack.go
@@ -0,0 +1,132 @@
+package hawapi
+
+import (
+ "github.com/google/uuid"
+)
+
+const soundtrackOrigin = "soundtracks"
+
+type Soundtrack struct {
+ UUID uuid.UUID `json:"uuid"`
+ Href string `json:"href"`
+ Name string `json:"name"`
+ Duration int64 `json:"duration"`
+ Artist string `json:"artist"`
+ Album string `json:"album,omitempty"`
+ ReleaseDate string `json:"release_date"`
+ Urls []string `json:"urls"`
+ Thumbnail string `json:"thumbnail,omitempty"`
+ Images []string `json:"images,omitempty"`
+ Sources []string `json:"sources,omitempty"`
+ CreatedAt string `json:"created_at"`
+ UpdatedAt string `json:"updated_at"`
+}
+
+type CreateSoundtrack struct {
+ Name string `json:"name,omitempty"`
+ Duration int64 `json:"duration,omitempty"`
+ Artist string `json:"artist,omitempty"`
+ Album string `json:"album,omitempty"`
+ ReleaseDate string `json:"release_date"`
+ Urls []string `json:"urls,omitempty"`
+ Thumbnail string `json:"thumbnail,omitempty"`
+ Images []string `json:"images,omitempty"`
+ Sources []string `json:"sources,omitempty"`
+}
+
+type PatchSoundtrack = CreateSoundtrack
+
+type SoundtrackResponse struct {
+ BaseResponse
+ Data Soundtrack `json:"data"`
+}
+
+type SoundtrackListResponse struct {
+ BaseResponse
+ Data []Soundtrack `json:"data"`
+}
+
+// ListSoundtracks will get all soundtracks
+func (c *Client) ListSoundtracks(options ...QueryOptions) (SoundtrackListResponse, error) {
+ var soundtracks []Soundtrack
+ var res SoundtrackListResponse
+
+ doRes, err := c.doGetRequest(soundtrackOrigin, options, &soundtracks)
+ if err != nil {
+ return res, err
+ }
+
+ res = SoundtrackListResponse{
+ BaseResponse: doRes,
+ Data: soundtracks,
+ }
+
+ return res, nil
+}
+
+// FindSoundtrack will get a single item by uuid
+func (c *Client) FindSoundtrack(id uuid.UUID) (SoundtrackResponse, error) {
+ var soundtrack Soundtrack
+ var res SoundtrackResponse
+
+ doRes, err := c.doGetRequest(soundtrackOrigin+"/"+id.String(), nil, &soundtrack)
+ if err != nil {
+ return res, err
+ }
+
+ res = SoundtrackResponse{
+ BaseResponse: doRes,
+ Data: soundtrack,
+ }
+
+ return res, nil
+}
+
+func (c *Client) RandomSoundtrack() (SoundtrackResponse, error) {
+ var soundtrack Soundtrack
+ var res SoundtrackResponse
+
+ doRes, err := c.doGetRequest(soundtrackOrigin+"/random", nil, &soundtrack)
+ if err != nil {
+ return res, err
+ }
+
+ res = SoundtrackResponse{
+ BaseResponse: doRes,
+ Data: soundtrack,
+ }
+
+ return res, nil
+}
+
+func (c *Client) CreateSoundtrack(s CreateSoundtrack) (Soundtrack, error) {
+ var soundtrack Soundtrack
+
+ err := c.doPostRequest(soundtrackOrigin, s, &soundtrack)
+ if err != nil {
+ return soundtrack, err
+ }
+
+ return soundtrack, nil
+}
+
+func (c *Client) PatchSoundtrack(id uuid.UUID, p PatchSoundtrack) (Soundtrack, error) {
+ var soundtrack Soundtrack
+
+ err := c.doPatchRequest(soundtrackOrigin+"/"+id.String(), &p)
+ if err != nil {
+ return soundtrack, err
+ }
+
+ res, err := c.FindSoundtrack(id)
+ if err != nil {
+ return soundtrack, err
+ }
+
+ soundtrack = res.Data
+ return soundtrack, nil
+}
+
+func (c *Client) DeleteSoundtrack(id uuid.UUID) error {
+ return c.doDeleteRequest(soundtrackOrigin + "/" + id.String())
+}