internal/database/typeddocument.go (79 lines of code) (raw):
// Copyright 2025 Microsoft Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package database
import (
"encoding/json"
"fmt"
"reflect"
"strings"
azcorearm "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos"
)
// typedDocumentError signifies a mismatched Type field and Properties type
// when attempting to unmarshal JSON-encoded data.
type typedDocumentError struct {
invalidType string
propertiesType string
}
func (e typedDocumentError) Error() string {
if e.invalidType == "" {
return "missing type"
}
return fmt.Sprintf("invalid type '%s' for %s", e.invalidType, e.propertiesType)
}
// typedDocument is a baseDocument with a ResourceType field to
// help distinguish heterogeneous items in a Cosmos DB container.
// The Properties field can be unmarshalled to any type that
// implements the DocumentProperties interface.
type typedDocument struct {
baseDocument
PartitionKey string `json:"partitionKey"`
ResourceType string `json:"resourceType"`
Properties json.RawMessage `json:"properties"`
}
// newTypedDocument returns a TypedDocument from a ResourceType.
func newTypedDocument(partitionKey string, resourceType azcorearm.ResourceType) *typedDocument {
return &typedDocument{
baseDocument: newBaseDocument(),
PartitionKey: strings.ToLower(partitionKey),
ResourceType: strings.ToLower(resourceType.String()),
}
}
// getPartitionKey returns an azcosmos.PartitionKey.
func (td *typedDocument) getPartitionKey() azcosmos.PartitionKey {
return azcosmos.NewPartitionKeyString(td.PartitionKey)
}
// validateType validates the type field against the given properties type.
// If type validation fails, validateType returns a typedDocumentError.
func (td *typedDocument) validateType(properties DocumentProperties) error {
for _, t := range properties.GetValidTypes() {
if strings.EqualFold(td.ResourceType, t) {
return nil
}
}
propertiesType := reflect.TypeOf(properties)
if propertiesType.Kind() == reflect.Pointer {
propertiesType = propertiesType.Elem()
}
return &typedDocumentError{
invalidType: td.ResourceType,
propertiesType: propertiesType.Name(),
}
}
// typedDocumentMarshal returns the JSON encoding of typedDoc with innerDoc
// as the properties value. First, however, typedDocumentMarshal validates
// the type field in typeDoc against innerDoc to ensure compatibility. If
// validation fails, typedDocumentMarshal returns a typedDocumentError.
func typedDocumentMarshal[T DocumentProperties](typedDoc *typedDocument, innerDoc *T) ([]byte, error) {
err := typedDoc.validateType(*innerDoc)
if err != nil {
return nil, err
}
data, err := json.Marshal(innerDoc)
if err != nil {
return nil, err
}
typedDoc.Properties = data
return json.Marshal(typedDoc)
}
// typedDocumentUnmarshal parses JSON-encoded data into a typedDocument,
// validates the type field against the type parameter T, and then parses
// the JSON-encoded properties data into an instance of type parameter T.
// If validation fails, typedDocumentUnmarshal returns a typedDocumentError.
func typedDocumentUnmarshal[T DocumentProperties](data []byte) (*typedDocument, *T, error) {
var typedDoc typedDocument
var innerDoc T
err := json.Unmarshal(data, &typedDoc)
if err != nil {
return nil, nil, err
}
err = typedDoc.validateType(innerDoc)
if err != nil {
return nil, nil, err
}
err = json.Unmarshal(typedDoc.Properties, &innerDoc)
if err != nil {
return nil, nil, err
}
return &typedDoc, &innerDoc, nil
}