package spec import ( "encoding/json" "fmt" "strings" "gopkg.in/yaml.v3" ) const ExtensionPrefix = "x-" // Extendable allows extensions to the OpenAPI Schema. // The field name MUST begin with `x-`, for example, `x-internal-id`. // Field names beginning `x-oai-` and `x-oas-` are reserved for uses defined by the OpenAPI Initiative. // The value can be null, a primitive, an array or an object. // // https://spec.openapis.org/oas/v3.1.0#specification-extensions // // Example: // // openapi: 3.1.0 // info: // title: Sample Pet Store App // summary: A pet store manager. // description: This is a sample server for a pet store. // version: 1.0.1 // x-build-data: 2006-01-02T15:04:05Z07:00 // x-build-commit-id: dac33af14d0d4a5f1c226141042ca7cefc6aeb75 type Extendable[T any] struct { Spec *T `json:"-" yaml:"-"` Extensions map[string]any `json:"-" yaml:"-"` } // NewExtendable creates new Extendable object for given spec func NewExtendable[T any](spec *T) *Extendable[T] { ext := Extendable[T]{ Spec: spec, Extensions: make(map[string]any), } return &ext } // MarshalJSON implements json.Marshaler interface. func (o *Extendable[T]) MarshalJSON() ([]byte, error) { var raw map[string]json.RawMessage exts, err := json.Marshal(&o.Extensions) if err != nil { return nil, fmt.Errorf("%T.Extensions: %w", o.Spec, err) } if err := json.Unmarshal(exts, &raw); err != nil { return nil, fmt.Errorf("%T(raw extensions): %w", o.Spec, err) } fields, err := json.Marshal(&o.Spec) if err != nil { return nil, fmt.Errorf("%T: %w", o.Spec, err) } if err := json.Unmarshal(fields, &raw); err != nil { return nil, fmt.Errorf("%T(raw fields): %w", o.Spec, err) } data, err := json.Marshal(&raw) if err != nil { return nil, fmt.Errorf("%T(raw): %w", o.Spec, err) } return data, nil } // UnmarshalJSON implements json.Unmarshaler interface. func (o *Extendable[T]) UnmarshalJSON(data []byte) error { var raw map[string]json.RawMessage if err := json.Unmarshal(data, &raw); err != nil { return fmt.Errorf("%T: %w", o.Spec, err) } o.Extensions = make(map[string]any) for name, value := range raw { if strings.HasPrefix(name, ExtensionPrefix) { var v any if err := json.Unmarshal(value, &v); err != nil { return fmt.Errorf("%T.Extensions.%s: %w", o.Spec, name, err) } o.Extensions[name] = v delete(raw, name) } } fields, err := json.Marshal(&raw) if err != nil { return fmt.Errorf("%T(raw): %w", o.Spec, err) } if err := json.Unmarshal(fields, &o.Spec); err != nil { return fmt.Errorf("%T: %w", o.Spec, err) } return nil } // MarshalYAML implements yaml.Marshaler interface. func (o *Extendable[T]) MarshalYAML() (any, error) { var raw map[string]any exts, err := yaml.Marshal(&o.Extensions) if err != nil { return nil, fmt.Errorf("%T.Extensions: %w", o.Spec, err) } if err := yaml.Unmarshal(exts, &raw); err != nil { return nil, fmt.Errorf("%T(raw extensions): %w", o.Spec, err) } fields, err := yaml.Marshal(&o.Spec) if err != nil { return nil, fmt.Errorf("%T: %w", o.Spec, err) } if err := yaml.Unmarshal(fields, &raw); err != nil { return nil, fmt.Errorf("%T(raw fields): %w", o.Spec, err) } return raw, nil } // UnmarshalYAML implements yaml.Unmarshaler interface. func (o *Extendable[T]) UnmarshalYAML(node *yaml.Node) error { var raw map[string]any if err := node.Decode(&raw); err != nil { return fmt.Errorf("%T: %w", o.Spec, err) } o.Extensions = make(map[string]any) for name, value := range raw { if strings.HasPrefix(name, ExtensionPrefix) { o.Extensions[name] = value delete(raw, name) } } fields, err := yaml.Marshal(&raw) if err != nil { return fmt.Errorf("%T(raw): %w", o.Spec, err) } if err := yaml.Unmarshal(fields, &o.Spec); err != nil { return fmt.Errorf("%T: %w", o.Spec, err) } return nil }