mirror of
https://github.com/LukeHagar/plexgo.git
synced 2025-12-06 12:37:46 +00:00
* `PlexApi.LibraryPlaylists.AddPlaylistItems()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Hubs.GetMetadataHubs()`: * `request.Request.OnlyTransient` **Changed** * `response.MediaContainer.Hub.[].Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.LibraryCollections.MoveCollectionItem()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.LibraryCollections.DeleteCollectionItem()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.LibraryCollections.AddCollectionItems()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Content.GetSonicallySimilar()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Butler.StopTask()`: `request.Request` **Changed** **Breaking** ⚠️ * `PlexApi.Butler.StartTask()`: `request.Request` **Changed** **Breaking** ⚠️ * `PlexApi.Content.GetSonicPath()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.DownloadQueue.GetItemDecision()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Hubs.GetAllHubs()`: * `request.Request.OnlyTransient` **Changed** * `response.MediaContainer.Hub.[].Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Hubs.GetContinueWatching()`: `response.MediaContainer.Hub.[].Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Hubs.GetPromotedHubs()`: `response.MediaContainer.Hub.[].Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Content.GetAllLeaves()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Hubs.GetPostplayHubs()`: * `request.Request.OnlyTransient` **Changed** * `response.MediaContainer.Hub.[].Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Hubs.GetRelatedHubs()`: * `request.Request.OnlyTransient` **Changed** * `response.MediaContainer.Hub.[].Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Hubs.GetSectionHubs()`: * `request.Request.OnlyTransient` **Changed** * `response.MediaContainer.Hub.[].Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Content.ListContent()`: * `request.Request` **Changed** * `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Content.GetAlbums()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Search.SearchHubs()`: `response.MediaContainer.Hub.[].Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Search.VoiceSearchHubs()`: * `request.Request.Type` **Changed** **Breaking** ⚠️ * `response.MediaContainer.Hub.[].Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Library.GetLibraryItems()`: * `request.Request.MediaQuery` **Changed** * `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Library.IngestTransientItem()`: * `request.Request` **Changed** * `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Library.GetLibraryMatches()`: * `request.Request` **Changed** **Breaking** ⚠️ * `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Content.GetMetadataItem()`: * `request.Request` **Changed** * `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Library.GetSections()`: `response.MediaContainer.Directory.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Library.AddSection()`: * `request.Request` **Changed** * `response.MediaContainer.Directory.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Library.GetTags()`: * `request.Request.Type` **Changed** **Breaking** ⚠️ * `PlexApi.Content.GetCollectionItems()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Library.GetAllItemLeaves()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Status.ListSessions()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.PlayQueue.MovePlayQueueItem()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Library.GetExtras()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.PlayQueue.DeletePlayQueueItem()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.PlayQueue.Unshuffle()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Library.ListMatches()`: * `request.Request.Manual` **Changed** * `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Library.ListSonicallySimilar()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.PlayQueue.ResetPlayQueue()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Library.GetRelatedItems()`: `response.MediaContainer.Hub.[].Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Library.ListSimilar()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.PlayQueue.ClearPlayQueue()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Library.GetItemTree()`: `response.MediaContainer.MetadataItem.[]` **Changed** **Breaking** ⚠️ * `PlexApi.PlayQueue.AddToPlayQueue()`: * `request.Request.Next` **Changed** * `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.PlayQueue.GetPlayQueue()`: * `request.Request` **Changed** * `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.LibraryPlaylists.MovePlaylistItem()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Library.GetPerson()`: `response.MediaContainer.Directory.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Library.ListPersonMedia()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.LibraryPlaylists.GetPlaylistGeneratorItems()`: `response.MediaContainer.Metadata` **Changed** **Breaking** ⚠️ * `PlexApi.Library.GetLibraryDetails()`: * `request.Request.IncludeDetails` **Changed** * `response.MediaContainer.Directory.[]` **Changed** **Breaking** ⚠️ * `PlexApi.LibraryPlaylists.ModifyPlaylistGenerator()`: * `request.Request.Item` **Changed** * `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Library.Autocomplete()`: * `request.Request.MediaQuery` **Changed** * `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Library.GetCollections()`: * `request.Request.MediaQuery` **Changed** * `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Library.GetCommon()`: * `request.Request.MediaQuery` **Changed** * `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.LibraryPlaylists.DeletePlaylistItem()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.LibraryPlaylists.ClearPlaylistItems()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.PlayQueue.Shuffle()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.LibraryPlaylists.CreatePlaylist()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Playlist.GetPlaylistItems()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Playlist.GetPlaylist()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Collections.CreateCollection()`: * `request.Request.Type` **Changed** **Breaking** ⚠️ * `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.DvRs.TuneChannel()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.LiveTv.GetSessions()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.LiveTv.GetLiveTvSession()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Playlist.ListPlaylists()`: `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Subscriptions.GetAllSubscriptions()`: * `request.Request` **Changed** * `response.MediaContainer.MediaSubscription.[].MediaGrabOperation.[].Metadata` **Changed** **Breaking** ⚠️ * `PlexApi.Subscriptions.CreateSubscription()`: `response.MediaContainer.MediaSubscription.[].MediaGrabOperation.[].Metadata` **Changed** **Breaking** ⚠️ * `PlexApi.Subscriptions.GetScheduledRecordings()`: `response.MediaContainer.MediaGrabOperation.[].Metadata` **Changed** **Breaking** ⚠️ * `PlexApi.Subscriptions.GetTemplate()`: `response.MediaContainer.SubscriptionTemplate.[].MediaSubscription.[].MediaGrabOperation.[].Metadata` **Changed** **Breaking** ⚠️ * `PlexApi.Subscriptions.GetSubscription()`: * `request.Request` **Changed** * `response.MediaContainer.MediaSubscription.[].MediaGrabOperation.[].Metadata` **Changed** **Breaking** ⚠️ * `PlexApi.Subscriptions.EditSubscriptionPreferences()`: `response.MediaContainer.MediaSubscription.[].MediaGrabOperation.[].Metadata` **Changed** **Breaking** ⚠️ * `PlexApi.Subscriptions.ReorderSubscription()`: `response.MediaContainer.MediaSubscription.[].MediaGrabOperation.[].Metadata` **Changed** **Breaking** ⚠️ * `PlexApi.Transcoder.MakeDecision()`: * `request.Request` **Changed** * `response.MediaContainer.Metadata.[]` **Changed** **Breaking** ⚠️ * `PlexApi.Library.RefreshItemsMetadata()`: * `request.Request.MarkUpdated` **Changed** * `PlexApi.Authentication.Post-Users-Sign-In-Data()`: **Added** * `PlexApi.Transcoder.StartTranscodeSession()`: `request.Request` **Changed** * `PlexApi.Devices.ModifyDevice()`: * `request.Request.Enabled` **Changed** * `PlexApi.Library.GetMediaPart()`: * `request.Request.Download` **Changed** * `PlexApi.Library.DetectIntros()`: * `request.Request.Force` **Changed** * `PlexApi.Library.RefreshSection()`: * `request.Request.Force` **Changed** * `PlexApi.LibraryPlaylists.UploadPlaylist()`: * `request.Request.Force` **Changed** * `PlexApi.Library.DeleteMediaItem()`: * `request.Request.Proxy` **Changed** * `PlexApi.Authentication.GetTokenDetails()`: **Added** * `PlexApi.Library.GetFirstCharacters()`: * `request.Request.MediaQuery` **Changed** * `PlexApi.Library.UpdateItems()`: * `request.Request.Field.locked` **Changed** * `PlexApi.Library.DeleteLibrarySection()`: * `request.Request.Async` **Changed** * `PlexApi.Library.SetStreamSelection()`: * `request.Request.AllParts` **Changed** * `PlexApi.PlayQueue.CreatePlayQueue()`: `request.Request` **Changed** * `PlexApi.Library.GetAugmentationStatus()`: * `request.Request.Wait` **Changed** * `PlexApi.Library.DetectVoiceActivity()`: `request.Request` **Changed** * `PlexApi.Transcoder.TranscodeImage()`: `request.Request` **Changed** * `PlexApi.Transcoder.TranscodeSubtitles()`: `request.Request` **Changed** * `PlexApi.Library.AddSubtitles()`: `request.Request` **Changed** * `PlexApi.Library.GetStream()`: * `request.Request.AutoAdjustSubtitle` **Changed** * `PlexApi.Library.StartBifGeneration()`: * `request.Request.Force` **Changed** * `PlexApi.Library.DetectCredits()`: `request.Request` **Changed** * `PlexApi.UltraBlur.GetImage()`: * `request.Request.Noise` **Changed** * `PlexApi.Library.GenerateThumbs()`: * `request.Request.Force` **Changed** * `PlexApi.Updater.ApplyUpdates()`: `request.Request` **Changed** * `PlexApi.Updater.CheckUpdates()`: * `request.Request.Download` **Changed** * `PlexApi.Library.DeleteMetadataItem()`: * `request.Request.Proxy` **Changed** * `PlexApi.Library.OptimizeDatabase()`: * `request.Request.Async` **Changed** * `PlexApi.Hubs.UpdateHubVisibility()`: `request.Request` **Changed** * `PlexApi.Hubs.CreateCustomHub()`: `request.Request` **Changed** * `PlexApi.Library.GetSectionImage()`: * `request.Request.MediaQuery` **Changed** * `PlexApi.DownloadQueue.AddDownloadQueueItems()`: `request.Request` **Changed** * `PlexApi.Timeline.Report()`: `request.Request` **Changed** * `PlexApi.General.GetSourceConnectionInformation()`: * `request.Request.Refresh` **Changed** * `PlexApi.Plex.Get-Server-Resources()`: **Added** * `PlexApi.Users.Get-Users()`: **Added**
914 lines
21 KiB
Go
914 lines
21 KiB
Go
// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
|
|
|
|
package utils
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"github.com/LukeHagar/plexgo/types"
|
|
)
|
|
|
|
func MarshalJSON(v interface{}, tag reflect.StructTag, topLevel bool) ([]byte, error) {
|
|
typ, val := dereferencePointers(reflect.TypeOf(v), reflect.ValueOf(v))
|
|
|
|
switch {
|
|
case isModelType(typ):
|
|
if topLevel {
|
|
return json.Marshal(v)
|
|
}
|
|
|
|
if isNil(typ, val) {
|
|
return []byte("null"), nil
|
|
}
|
|
|
|
out := map[string]json.RawMessage{}
|
|
|
|
for i := 0; i < typ.NumField(); i++ {
|
|
field := typ.Field(i)
|
|
fieldVal := val.Field(i)
|
|
|
|
fieldName := field.Name
|
|
|
|
omitEmpty := false
|
|
omitZero := false
|
|
jsonTag := field.Tag.Get("json")
|
|
if jsonTag != "" {
|
|
for _, tag := range strings.Split(jsonTag, ",") {
|
|
if tag == "omitempty" {
|
|
omitEmpty = true
|
|
} else if tag == "omitzero" {
|
|
omitZero = true
|
|
} else {
|
|
fieldName = tag
|
|
}
|
|
}
|
|
}
|
|
|
|
if (omitEmpty || omitZero) && field.Tag.Get("const") == "" {
|
|
// Both omitempty and omitzero skip zero values (including nil)
|
|
if isNil(field.Type, fieldVal) {
|
|
continue
|
|
}
|
|
|
|
if omitZero && fieldVal.IsZero() {
|
|
continue
|
|
}
|
|
|
|
}
|
|
|
|
if !field.IsExported() && field.Tag.Get("const") == "" {
|
|
continue
|
|
}
|
|
|
|
additionalProperties := field.Tag.Get("additionalProperties")
|
|
if fieldName == "-" && additionalProperties == "" {
|
|
continue
|
|
}
|
|
|
|
if additionalProperties == "true" {
|
|
if isNil(field.Type, fieldVal) {
|
|
continue
|
|
}
|
|
fieldVal := trueReflectValue(fieldVal)
|
|
if fieldVal.Type().Kind() != reflect.Map {
|
|
return nil, fmt.Errorf("additionalProperties must be a map")
|
|
}
|
|
|
|
for _, key := range fieldVal.MapKeys() {
|
|
r, err := marshalValue(fieldVal.MapIndex(key).Interface(), field.Tag)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out[key.String()] = r
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
var fv interface{}
|
|
|
|
if field.IsExported() {
|
|
fv = fieldVal.Interface()
|
|
} else {
|
|
pt := reflect.New(typ).Elem()
|
|
pt.Set(val)
|
|
|
|
pf := pt.Field(i)
|
|
|
|
fv = reflect.NewAt(pf.Type(), unsafe.Pointer(pf.UnsafeAddr())).Elem().Interface()
|
|
}
|
|
|
|
r, err := marshalValue(fv, field.Tag)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out[fieldName] = r
|
|
}
|
|
|
|
return json.Marshal(out)
|
|
default:
|
|
return marshalValue(v, tag)
|
|
}
|
|
}
|
|
|
|
func UnmarshalJSON(b []byte, v interface{}, tag reflect.StructTag, topLevel bool, requiredFields []string) error {
|
|
if reflect.TypeOf(v).Kind() != reflect.Ptr {
|
|
return errors.New("v must be a pointer")
|
|
}
|
|
|
|
typ, val := dereferencePointers(reflect.TypeOf(v), reflect.ValueOf(v))
|
|
|
|
switch {
|
|
case isModelType(typ):
|
|
if topLevel || bytes.Equal(b, []byte("null")) {
|
|
return json.Unmarshal(b, v)
|
|
}
|
|
|
|
var unmarshaled map[string]json.RawMessage
|
|
|
|
if err := json.Unmarshal(b, &unmarshaled); err != nil {
|
|
return err
|
|
}
|
|
|
|
missingFields := []string{}
|
|
for _, requiredField := range requiredFields {
|
|
if _, ok := unmarshaled[requiredField]; !ok {
|
|
missingFields = append(missingFields, requiredField)
|
|
}
|
|
}
|
|
if len(missingFields) > 0 {
|
|
return fmt.Errorf("missing required fields: %s", strings.Join(missingFields, ", "))
|
|
}
|
|
|
|
var additionalPropertiesField *reflect.StructField
|
|
var additionalPropertiesValue *reflect.Value
|
|
|
|
for i := 0; i < typ.NumField(); i++ {
|
|
field := typ.Field(i)
|
|
fieldVal := val.Field(i)
|
|
|
|
fieldName := field.Name
|
|
|
|
jsonTag := field.Tag.Get("json")
|
|
if jsonTag != "" {
|
|
for _, tag := range strings.Split(jsonTag, ",") {
|
|
if tag != "omitempty" && tag != "omitzero" {
|
|
fieldName = tag
|
|
}
|
|
}
|
|
}
|
|
|
|
if field.Tag.Get("additionalProperties") == "true" {
|
|
additionalPropertiesField = &field
|
|
additionalPropertiesValue = &fieldVal
|
|
continue
|
|
}
|
|
|
|
// If we receive a value for a const field ignore it but mark it as unmarshaled
|
|
if field.Tag.Get("const") != "" {
|
|
if r, ok := unmarshaled[fieldName]; ok {
|
|
val := string(r)
|
|
|
|
if strings.HasPrefix(val, `"`) && strings.HasSuffix(val, `"`) {
|
|
var err error
|
|
val, err = strconv.Unquote(val)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to unquote const field `%s` value `%s`: %w", fieldName, val, err)
|
|
}
|
|
}
|
|
constValue := field.Tag.Get("const")
|
|
if val != constValue {
|
|
return fmt.Errorf("const field `%s` does not match expected value `%s` got `%s`", fieldName, constValue, val)
|
|
}
|
|
|
|
delete(unmarshaled, fieldName)
|
|
}
|
|
} else if !field.IsExported() {
|
|
continue
|
|
}
|
|
|
|
value, ok := unmarshaled[fieldName]
|
|
if !ok {
|
|
defaultTag, defaultOk := field.Tag.Lookup("default")
|
|
if defaultOk {
|
|
value = handleDefaultConstValue(defaultTag, fieldVal.Interface(), field.Tag)
|
|
ok = true
|
|
}
|
|
} else {
|
|
delete(unmarshaled, fieldName)
|
|
}
|
|
|
|
if ok {
|
|
if err := unmarshalValue(value, fieldVal, field.Tag); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
keys := make([]string, 0, len(unmarshaled))
|
|
for k := range unmarshaled {
|
|
keys = append(keys, k)
|
|
}
|
|
|
|
if len(keys) > 0 {
|
|
if additionalPropertiesField != nil && additionalPropertiesValue != nil {
|
|
typeOfMap := additionalPropertiesField.Type
|
|
if additionalPropertiesValue.Type().Kind() == reflect.Interface {
|
|
typeOfMap = reflect.TypeOf(map[string]interface{}{})
|
|
} else if additionalPropertiesValue.Type().Kind() != reflect.Map {
|
|
return fmt.Errorf("additionalProperties must be a map")
|
|
}
|
|
|
|
mapValue := reflect.MakeMap(typeOfMap)
|
|
|
|
for key, value := range unmarshaled {
|
|
val := reflect.New(typeOfMap.Elem())
|
|
|
|
if err := unmarshalValue(value, val, additionalPropertiesField.Tag); err != nil {
|
|
return err
|
|
}
|
|
|
|
if val.Elem().Type().String() == typeOfMap.Elem().String() {
|
|
mapValue.SetMapIndex(reflect.ValueOf(key), val.Elem())
|
|
} else {
|
|
mapValue.SetMapIndex(reflect.ValueOf(key), trueReflectValue(val))
|
|
}
|
|
|
|
}
|
|
if additionalPropertiesValue.Type().Kind() == reflect.Interface {
|
|
additionalPropertiesValue.Set(mapValue)
|
|
} else {
|
|
additionalPropertiesValue.Set(mapValue)
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
return unmarshalValue(b, reflect.ValueOf(v), tag)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func marshalValue(v interface{}, tag reflect.StructTag) (json.RawMessage, error) {
|
|
constTag := tag.Get("const")
|
|
if constTag != "" {
|
|
return handleDefaultConstValue(constTag, v, tag), nil
|
|
}
|
|
|
|
if isNil(reflect.TypeOf(v), reflect.ValueOf(v)) {
|
|
defaultTag, ok := tag.Lookup("default")
|
|
if ok {
|
|
return handleDefaultConstValue(defaultTag, v, tag), nil
|
|
}
|
|
|
|
return []byte("null"), nil
|
|
}
|
|
|
|
typ, val := dereferencePointers(reflect.TypeOf(v), reflect.ValueOf(v))
|
|
switch typ.Kind() {
|
|
case reflect.Int64:
|
|
format := tag.Get("integer")
|
|
if format == "string" {
|
|
b := val.Interface().(int64)
|
|
return []byte(fmt.Sprintf(`"%d"`, b)), nil
|
|
}
|
|
case reflect.Float64:
|
|
format := tag.Get("number")
|
|
if format == "string" {
|
|
b := val.Interface().(float64)
|
|
return []byte(fmt.Sprintf(`"%g"`, b)), nil
|
|
}
|
|
case reflect.Map:
|
|
if isNil(typ, val) {
|
|
return []byte("null"), nil
|
|
}
|
|
|
|
// Check if the map implements json.Marshaler (like optionalnullable.OptionalNullable[T])
|
|
if marshaler, ok := val.Interface().(json.Marshaler); ok {
|
|
return marshaler.MarshalJSON()
|
|
}
|
|
|
|
out := map[string]json.RawMessage{}
|
|
|
|
for _, key := range val.MapKeys() {
|
|
itemVal := val.MapIndex(key)
|
|
|
|
if isNil(itemVal.Type(), itemVal) {
|
|
out[key.String()] = []byte("null")
|
|
continue
|
|
}
|
|
|
|
r, err := marshalValue(itemVal.Interface(), tag)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out[key.String()] = r
|
|
}
|
|
|
|
return json.Marshal(out)
|
|
case reflect.Slice, reflect.Array:
|
|
if isNil(typ, val) {
|
|
return []byte("null"), nil
|
|
}
|
|
|
|
out := []json.RawMessage{}
|
|
|
|
for i := 0; i < val.Len(); i++ {
|
|
itemVal := val.Index(i)
|
|
|
|
if isNil(itemVal.Type(), itemVal) {
|
|
out = append(out, []byte("null"))
|
|
continue
|
|
}
|
|
|
|
r, err := marshalValue(itemVal.Interface(), tag)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out = append(out, r)
|
|
}
|
|
|
|
return json.Marshal(out)
|
|
case reflect.Struct:
|
|
switch typ {
|
|
case reflect.TypeOf(time.Time{}):
|
|
return []byte(fmt.Sprintf(`"%s"`, val.Interface().(time.Time).Format(time.RFC3339Nano))), nil
|
|
case reflect.TypeOf(big.Int{}):
|
|
format := tag.Get("bigint")
|
|
if format == "string" {
|
|
b := val.Interface().(big.Int)
|
|
return []byte(fmt.Sprintf(`"%s"`, (&b).String())), nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return json.Marshal(v)
|
|
}
|
|
|
|
func handleDefaultConstValue(tagValue string, val interface{}, tag reflect.StructTag) json.RawMessage {
|
|
if tagValue == "null" {
|
|
return []byte("null")
|
|
}
|
|
|
|
typ := dereferenceTypePointer(reflect.TypeOf(val))
|
|
switch typ {
|
|
case reflect.TypeOf(time.Time{}):
|
|
return []byte(fmt.Sprintf(`"%s"`, tagValue))
|
|
case reflect.TypeOf(big.Int{}):
|
|
bigIntTag := tag.Get("bigint")
|
|
if bigIntTag == "string" {
|
|
return []byte(fmt.Sprintf(`"%s"`, tagValue))
|
|
}
|
|
case reflect.TypeOf(int64(0)):
|
|
format := tag.Get("integer")
|
|
if format == "string" {
|
|
return []byte(fmt.Sprintf(`"%s"`, tagValue))
|
|
}
|
|
case reflect.TypeOf(float64(0)):
|
|
format := tag.Get("number")
|
|
if format == "string" {
|
|
return []byte(fmt.Sprintf(`"%s"`, tagValue))
|
|
}
|
|
case reflect.TypeOf(types.Date{}):
|
|
return []byte(fmt.Sprintf(`"%s"`, tagValue))
|
|
default:
|
|
if typ.Kind() == reflect.String {
|
|
return []byte(fmt.Sprintf("%q", tagValue))
|
|
}
|
|
}
|
|
|
|
return []byte(tagValue)
|
|
}
|
|
|
|
func unmarshalValue(value json.RawMessage, v reflect.Value, tag reflect.StructTag) error {
|
|
if bytes.Equal(value, []byte("null")) {
|
|
if v.CanAddr() {
|
|
return json.Unmarshal(value, v.Addr().Interface())
|
|
} else {
|
|
return json.Unmarshal(value, v.Interface())
|
|
}
|
|
}
|
|
|
|
typ := dereferenceTypePointer(v.Type())
|
|
|
|
switch typ.Kind() {
|
|
case reflect.Int64:
|
|
var b int64
|
|
|
|
format := tag.Get("integer")
|
|
if format == "string" {
|
|
var s string
|
|
if err := json.Unmarshal(value, &s); err != nil {
|
|
return err
|
|
}
|
|
|
|
var err error
|
|
b, err = strconv.ParseInt(s, 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse string as int64: %w", err)
|
|
}
|
|
if v.Kind() == reflect.Ptr {
|
|
if v.IsNil() {
|
|
v.Set(reflect.New(typ))
|
|
}
|
|
v = v.Elem()
|
|
}
|
|
|
|
v.Set(reflect.ValueOf(b))
|
|
return nil
|
|
}
|
|
case reflect.Float64:
|
|
var b float64
|
|
|
|
format := tag.Get("number")
|
|
if format == "string" {
|
|
var s string
|
|
if err := json.Unmarshal(value, &s); err != nil {
|
|
return err
|
|
}
|
|
|
|
var err error
|
|
b, err = strconv.ParseFloat(s, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse string as float64: %w", err)
|
|
}
|
|
|
|
if v.Kind() == reflect.Ptr {
|
|
if v.IsNil() {
|
|
v.Set(reflect.New(typ))
|
|
}
|
|
v = v.Elem()
|
|
}
|
|
|
|
v.Set(reflect.ValueOf(b))
|
|
return nil
|
|
}
|
|
case reflect.Map:
|
|
if bytes.Equal(value, []byte("null")) || !isComplexValueType(dereferenceTypePointer(typ.Elem())) {
|
|
if v.CanAddr() {
|
|
return json.Unmarshal(value, v.Addr().Interface())
|
|
} else {
|
|
return json.Unmarshal(value, v.Interface())
|
|
}
|
|
}
|
|
|
|
var unmarshaled map[string]json.RawMessage
|
|
|
|
if err := json.Unmarshal(value, &unmarshaled); err != nil {
|
|
return err
|
|
}
|
|
|
|
m := reflect.MakeMap(typ)
|
|
|
|
for k, value := range unmarshaled {
|
|
itemVal := reflect.New(typ.Elem())
|
|
|
|
if err := unmarshalValue(value, itemVal, tag); err != nil {
|
|
return err
|
|
}
|
|
|
|
m.SetMapIndex(reflect.ValueOf(k), itemVal.Elem())
|
|
}
|
|
|
|
v.Set(m)
|
|
return nil
|
|
case reflect.Slice, reflect.Array:
|
|
var unmarshaled []json.RawMessage
|
|
|
|
if err := json.Unmarshal(value, &unmarshaled); err != nil {
|
|
return err
|
|
}
|
|
|
|
arrVal := reflect.MakeSlice(typ, len(unmarshaled), len(unmarshaled))
|
|
|
|
for index, value := range unmarshaled {
|
|
itemVal := reflect.New(typ.Elem())
|
|
|
|
if err := unmarshalValue(value, itemVal, tag); err != nil {
|
|
return err
|
|
}
|
|
|
|
arrVal.Index(index).Set(itemVal.Elem())
|
|
}
|
|
|
|
if v.Kind() == reflect.Pointer {
|
|
if v.IsNil() {
|
|
v.Set(reflect.New(typ))
|
|
}
|
|
v = v.Elem()
|
|
}
|
|
|
|
v.Set(arrVal)
|
|
return nil
|
|
case reflect.Struct:
|
|
switch typ {
|
|
case reflect.TypeOf(time.Time{}):
|
|
var s string
|
|
if err := json.Unmarshal(value, &s); err != nil {
|
|
return err
|
|
}
|
|
|
|
t, err := time.Parse(time.RFC3339Nano, s)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse string as time.Time: %w", err)
|
|
}
|
|
|
|
if v.Kind() == reflect.Ptr {
|
|
if v.IsNil() {
|
|
v.Set(reflect.New(typ))
|
|
}
|
|
v = v.Elem()
|
|
}
|
|
|
|
v.Set(reflect.ValueOf(t))
|
|
return nil
|
|
case reflect.TypeOf(big.Int{}):
|
|
var b *big.Int
|
|
|
|
format := tag.Get("bigint")
|
|
if format == "string" {
|
|
var s string
|
|
if err := json.Unmarshal(value, &s); err != nil {
|
|
return err
|
|
}
|
|
|
|
var ok bool
|
|
b, ok = new(big.Int).SetString(s, 10)
|
|
if !ok {
|
|
return fmt.Errorf("failed to parse string as big.Int")
|
|
}
|
|
} else {
|
|
if err := json.Unmarshal(value, &b); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Ptr {
|
|
v = v.Elem()
|
|
}
|
|
|
|
v.Set(reflect.ValueOf(b))
|
|
return nil
|
|
case reflect.TypeOf(types.Date{}):
|
|
var s string
|
|
|
|
if err := json.Unmarshal(value, &s); err != nil {
|
|
return err
|
|
}
|
|
|
|
d, err := types.DateFromString(s)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse string as types.Date: %w", err)
|
|
}
|
|
|
|
if v.Kind() == reflect.Ptr {
|
|
if v.IsNil() {
|
|
v.Set(reflect.New(typ))
|
|
}
|
|
v = v.Elem()
|
|
}
|
|
|
|
v.Set(reflect.ValueOf(d))
|
|
return nil
|
|
}
|
|
}
|
|
|
|
var val interface{}
|
|
|
|
if v.CanAddr() {
|
|
val = v.Addr().Interface()
|
|
} else {
|
|
val = v.Interface()
|
|
}
|
|
|
|
return json.Unmarshal(value, val)
|
|
}
|
|
|
|
func dereferencePointers(typ reflect.Type, val reflect.Value) (reflect.Type, reflect.Value) {
|
|
if typ.Kind() == reflect.Ptr {
|
|
typ = typ.Elem()
|
|
val = val.Elem()
|
|
} else {
|
|
return typ, val
|
|
}
|
|
|
|
return dereferencePointers(typ, val)
|
|
}
|
|
|
|
func dereferenceTypePointer(typ reflect.Type) reflect.Type {
|
|
if typ.Kind() == reflect.Ptr {
|
|
typ = typ.Elem()
|
|
} else {
|
|
return typ
|
|
}
|
|
|
|
return dereferenceTypePointer(typ)
|
|
}
|
|
|
|
func isComplexValueType(typ reflect.Type) bool {
|
|
switch typ.Kind() {
|
|
case reflect.Struct:
|
|
switch typ {
|
|
case reflect.TypeOf(time.Time{}):
|
|
fallthrough
|
|
case reflect.TypeOf(big.Int{}):
|
|
fallthrough
|
|
case reflect.TypeOf(types.Date{}):
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func isModelType(typ reflect.Type) bool {
|
|
if isComplexValueType(typ) {
|
|
return false
|
|
}
|
|
|
|
if typ.Kind() == reflect.Struct {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// CalculateJSONSize returns the byte size of the JSON representation of a value.
|
|
// This is used to determine which union type variant has the most data.
|
|
func CalculateJSONSize(v interface{}) int {
|
|
data, err := json.Marshal(v)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return len(data)
|
|
}
|
|
|
|
// UnionCandidate represents a candidate type during union deserialization
|
|
type UnionCandidate struct {
|
|
FieldCount int
|
|
InexactCount int // Count of fields with unknown/unrecognized enum values
|
|
Size int
|
|
Type any // The union type enum value
|
|
Value any // The unmarshaled value
|
|
}
|
|
|
|
// FieldCounts holds the result of counting fields in a value
|
|
type FieldCounts struct {
|
|
Total int // Total number of populated fields
|
|
Inexact int // Number of fields with unknown/unrecognized open enum values
|
|
}
|
|
|
|
// CountFieldsWithInexact recursively counts fields and tracks inexact matches (unknown open enum values).
|
|
func CountFieldsWithInexact(v interface{}) FieldCounts {
|
|
if v == nil {
|
|
return FieldCounts{}
|
|
}
|
|
|
|
typ := reflect.TypeOf(v)
|
|
val := reflect.ValueOf(v)
|
|
|
|
// Dereference pointers
|
|
for typ.Kind() == reflect.Ptr {
|
|
if val.IsNil() {
|
|
return FieldCounts{}
|
|
}
|
|
typ = typ.Elem()
|
|
val = val.Elem()
|
|
}
|
|
|
|
return countFieldsRecursive(typ, val)
|
|
}
|
|
|
|
// PickBestCandidate selects the best union type candidate using a multi-stage filtering approach:
|
|
// 1. If multiple candidates, filter by field count (keep only those with max field count)
|
|
// 2. If still multiple, filter by inexact count (keep only those with min inexact count)
|
|
// 3. If still multiple, filter by JSON size (keep only those with max size)
|
|
// 4. Return the first remaining candidate
|
|
func PickBestCandidate(candidates []UnionCandidate) *UnionCandidate {
|
|
if len(candidates) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if len(candidates) == 1 {
|
|
return &candidates[0]
|
|
}
|
|
|
|
// Filter by field count if we have multiple candidates
|
|
if len(candidates) > 1 {
|
|
maxFieldCount := -1
|
|
for i := range candidates {
|
|
fieldCounts := CountFieldsWithInexact(candidates[i].Value)
|
|
candidates[i].FieldCount = fieldCounts.Total
|
|
candidates[i].InexactCount = fieldCounts.Inexact
|
|
if candidates[i].FieldCount > maxFieldCount {
|
|
maxFieldCount = candidates[i].FieldCount
|
|
}
|
|
}
|
|
|
|
// Keep only candidates with maximum field count
|
|
filtered := make([]UnionCandidate, 0, len(candidates))
|
|
for _, c := range candidates {
|
|
if c.FieldCount == maxFieldCount {
|
|
filtered = append(filtered, c)
|
|
}
|
|
}
|
|
candidates = filtered
|
|
}
|
|
|
|
if len(candidates) == 1 {
|
|
return &candidates[0]
|
|
}
|
|
|
|
// Filter by inexact count if we still have multiple candidates
|
|
// Prefer candidates with fewer unknown/unrecognized enum values
|
|
if len(candidates) > 1 {
|
|
minInexactCount := int(^uint(0) >> 1) // max int
|
|
for _, c := range candidates {
|
|
if c.InexactCount < minInexactCount {
|
|
minInexactCount = c.InexactCount
|
|
}
|
|
}
|
|
|
|
// Keep only candidates with minimum inexact count
|
|
filtered := make([]UnionCandidate, 0, len(candidates))
|
|
for _, c := range candidates {
|
|
if c.InexactCount == minInexactCount {
|
|
filtered = append(filtered, c)
|
|
}
|
|
}
|
|
candidates = filtered
|
|
}
|
|
|
|
if len(candidates) == 1 {
|
|
return &candidates[0]
|
|
}
|
|
|
|
// Filter by JSON size if we still have multiple candidates
|
|
if len(candidates) > 1 {
|
|
maxSize := -1
|
|
for i := range candidates {
|
|
candidates[i].Size = CalculateJSONSize(candidates[i].Value)
|
|
if candidates[i].Size > maxSize {
|
|
maxSize = candidates[i].Size
|
|
}
|
|
}
|
|
|
|
// Keep only candidates with maximum size
|
|
filtered := make([]UnionCandidate, 0, len(candidates))
|
|
for _, c := range candidates {
|
|
if c.Size == maxSize {
|
|
filtered = append(filtered, c)
|
|
}
|
|
}
|
|
candidates = filtered
|
|
}
|
|
|
|
// Pick the first remaining candidate
|
|
return &candidates[0]
|
|
}
|
|
|
|
func countFieldsRecursive(typ reflect.Type, val reflect.Value) FieldCounts {
|
|
counts := FieldCounts{}
|
|
|
|
// Check if the value has an IsExact() method (for open enums)
|
|
// Try both the value and its pointer
|
|
if val.CanInterface() && val.CanAddr() {
|
|
ptrVal := val.Addr()
|
|
if method := ptrVal.MethodByName("IsExact"); method.IsValid() {
|
|
results := method.Call(nil)
|
|
if len(results) == 1 && results[0].Kind() == reflect.Bool {
|
|
isExact := results[0].Bool()
|
|
counts.Total = 1
|
|
if !isExact {
|
|
counts.Inexact = 1 // Unknown enum value
|
|
}
|
|
return counts
|
|
}
|
|
}
|
|
}
|
|
|
|
switch typ.Kind() {
|
|
case reflect.Struct:
|
|
// Handle special types
|
|
switch typ {
|
|
case reflect.TypeOf(time.Time{}):
|
|
if !val.Interface().(time.Time).IsZero() {
|
|
return FieldCounts{Total: 1}
|
|
}
|
|
return FieldCounts{}
|
|
case reflect.TypeOf(big.Int{}):
|
|
b := val.Interface().(big.Int)
|
|
if b.Sign() != 0 {
|
|
return FieldCounts{Total: 1}
|
|
}
|
|
return FieldCounts{}
|
|
case reflect.TypeOf(types.Date{}):
|
|
// Date is always counted if it exists
|
|
return FieldCounts{Total: 1}
|
|
}
|
|
|
|
// For regular structs, count non-zero fields
|
|
for i := 0; i < typ.NumField(); i++ {
|
|
field := typ.Field(i)
|
|
fieldVal := val.Field(i)
|
|
|
|
// Skip unexported fields and const fields
|
|
if !field.IsExported() || field.Tag.Get("const") != "" {
|
|
continue
|
|
}
|
|
|
|
// Skip fields tagged with json:"-"
|
|
jsonTag := field.Tag.Get("json")
|
|
if jsonTag == "-" {
|
|
continue
|
|
}
|
|
|
|
fieldTyp := field.Type
|
|
// Dereference pointer types for the field
|
|
for fieldTyp.Kind() == reflect.Ptr {
|
|
if fieldVal.IsNil() {
|
|
break
|
|
}
|
|
fieldTyp = fieldTyp.Elem()
|
|
fieldVal = fieldVal.Elem()
|
|
}
|
|
|
|
if !isNil(field.Type, val.Field(i)) {
|
|
fieldCounts := countFieldsRecursive(fieldTyp, fieldVal)
|
|
counts.Total += fieldCounts.Total
|
|
counts.Inexact += fieldCounts.Inexact
|
|
}
|
|
}
|
|
|
|
case reflect.Slice, reflect.Array:
|
|
if val.IsNil() || val.Len() == 0 {
|
|
return FieldCounts{}
|
|
}
|
|
// Count each array/slice element
|
|
for i := 0; i < val.Len(); i++ {
|
|
itemVal := val.Index(i)
|
|
itemTyp := itemVal.Type()
|
|
|
|
// Dereference pointer types
|
|
for itemTyp.Kind() == reflect.Ptr {
|
|
if itemVal.IsNil() {
|
|
break
|
|
}
|
|
itemTyp = itemTyp.Elem()
|
|
itemVal = itemVal.Elem()
|
|
}
|
|
|
|
if !isNil(itemTyp, itemVal) {
|
|
itemCounts := countFieldsRecursive(itemTyp, itemVal)
|
|
counts.Total += itemCounts.Total
|
|
counts.Inexact += itemCounts.Inexact
|
|
}
|
|
}
|
|
|
|
case reflect.String:
|
|
if val.String() != "" {
|
|
counts.Total = 1
|
|
}
|
|
|
|
case reflect.Bool:
|
|
// Bools always count as a field (even if false)
|
|
counts.Total = 1
|
|
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
if val.Int() != 0 {
|
|
counts.Total = 1
|
|
}
|
|
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
if val.Uint() != 0 {
|
|
counts.Total = 1
|
|
}
|
|
|
|
case reflect.Float32, reflect.Float64:
|
|
if val.Float() != 0 {
|
|
counts.Total = 1
|
|
}
|
|
|
|
default:
|
|
// For any other type, if it's not zero, count it as 1
|
|
if !val.IsZero() {
|
|
counts.Total = 1
|
|
}
|
|
}
|
|
|
|
return counts
|
|
}
|