Files
libopenapi/datamodel/low/v3/components.go
quobix 8717b3cd33 An enormous amount of surgery on the low level model.
Every `Build()` method now requires a `context.Context`. This is so the rolodex knows where to resolve from when locating relative links. Without knowing where we are, there is no way to resolve anything. This new mechanism allows the model to recurse across as many files as required to locate references, without loosing track of where we are in the process.

Signed-off-by: quobix <dave@quobix.com>
2023-10-23 15:04:34 -04:00

328 lines
10 KiB
Go

// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley
// SPDX-License-Identifier: MIT
package v3
import (
"context"
"crypto/sha256"
"fmt"
"sort"
"strings"
"sync"
"github.com/pb33f/libopenapi/datamodel"
"github.com/pb33f/libopenapi/datamodel/low"
"github.com/pb33f/libopenapi/datamodel/low/base"
"github.com/pb33f/libopenapi/index"
"github.com/pb33f/libopenapi/utils"
"gopkg.in/yaml.v3"
)
// Components represents a low-level OpenAPI 3+ Components Object, that is backed by a low-level one.
//
// Holds a set of reusable objects for different aspects of the OAS. All objects defined within the components object
// will have no effect on the API unless they are explicitly referenced from properties outside the components object.
// - https://spec.openapis.org/oas/v3.1.0#components-object
type Components struct {
Schemas low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*base.SchemaProxy]]
Responses low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Response]]
Parameters low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Parameter]]
Examples low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*base.Example]]
RequestBodies low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*RequestBody]]
Headers low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Header]]
SecuritySchemes low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*SecurityScheme]]
Links low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Link]]
Callbacks low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Callback]]
Extensions map[low.KeyReference[string]]low.ValueReference[any]
*low.Reference
}
type componentBuildResult[T any] struct {
key low.KeyReference[string]
value low.ValueReference[T]
}
type componentInput struct {
node *yaml.Node
currentLabel *yaml.Node
}
// GetExtensions returns all Components extensions and satisfies the low.HasExtensions interface.
func (co *Components) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return co.Extensions
}
// Hash will return a consistent SHA256 Hash of the Encoding object
func (co *Components) Hash() [32]byte {
var f []string
generateHashForObjectMap(co.Schemas.Value, &f)
generateHashForObjectMap(co.Responses.Value, &f)
generateHashForObjectMap(co.Parameters.Value, &f)
generateHashForObjectMap(co.Examples.Value, &f)
generateHashForObjectMap(co.RequestBodies.Value, &f)
generateHashForObjectMap(co.Headers.Value, &f)
generateHashForObjectMap(co.SecuritySchemes.Value, &f)
generateHashForObjectMap(co.Links.Value, &f)
generateHashForObjectMap(co.Callbacks.Value, &f)
keys := make([]string, len(co.Extensions))
z := 0
for k := range co.Extensions {
keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(co.Extensions[k].Value))))
z++
}
sort.Strings(keys)
f = append(f, keys...)
return sha256.Sum256([]byte(strings.Join(f, "|")))
}
func generateHashForObjectMap[T any](collection map[low.KeyReference[string]]low.ValueReference[T], hash *[]string) {
if collection == nil {
return
}
l := make([]string, len(collection))
keys := make(map[string]low.ValueReference[T])
z := 0
for k := range collection {
keys[k.Value] = collection[k]
l[z] = k.Value
z++
}
sort.Strings(l)
for k := range l {
*hash = append(*hash, low.GenerateHashString(keys[l[k]].Value))
}
}
// FindExtension attempts to locate an extension with the supplied key
func (co *Components) FindExtension(ext string) *low.ValueReference[any] {
return low.FindItemInMap[any](ext, co.Extensions)
}
// FindSchema attempts to locate a SchemaProxy from 'schemas' with a specific name
func (co *Components) FindSchema(schema string) *low.ValueReference[*base.SchemaProxy] {
return low.FindItemInMap[*base.SchemaProxy](schema, co.Schemas.Value)
}
// FindResponse attempts to locate a Response from 'responses' with a specific name
func (co *Components) FindResponse(response string) *low.ValueReference[*Response] {
return low.FindItemInMap[*Response](response, co.Responses.Value)
}
// FindParameter attempts to locate a Parameter from 'parameters' with a specific name
func (co *Components) FindParameter(response string) *low.ValueReference[*Parameter] {
return low.FindItemInMap[*Parameter](response, co.Parameters.Value)
}
// FindSecurityScheme attempts to locate a SecurityScheme from 'securitySchemes' with a specific name
func (co *Components) FindSecurityScheme(sScheme string) *low.ValueReference[*SecurityScheme] {
return low.FindItemInMap[*SecurityScheme](sScheme, co.SecuritySchemes.Value)
}
// FindExample attempts tp
func (co *Components) FindExample(example string) *low.ValueReference[*base.Example] {
return low.FindItemInMap[*base.Example](example, co.Examples.Value)
}
func (co *Components) FindRequestBody(requestBody string) *low.ValueReference[*RequestBody] {
return low.FindItemInMap[*RequestBody](requestBody, co.RequestBodies.Value)
}
func (co *Components) FindHeader(header string) *low.ValueReference[*Header] {
return low.FindItemInMap[*Header](header, co.Headers.Value)
}
func (co *Components) FindLink(link string) *low.ValueReference[*Link] {
return low.FindItemInMap[*Link](link, co.Links.Value)
}
func (co *Components) FindCallback(callback string) *low.ValueReference[*Callback] {
return low.FindItemInMap[*Callback](callback, co.Callbacks.Value)
}
// Build converts root YAML node containing components to low level model.
// Process each component in parallel.
func (co *Components) Build(ctx context.Context, root *yaml.Node, idx *index.SpecIndex) error {
root = utils.NodeAlias(root)
utils.CheckForMergeNodes(root)
co.Reference = new(low.Reference)
co.Extensions = low.ExtractExtensions(root)
var reterr error
var ceMutex sync.Mutex
var wg sync.WaitGroup
wg.Add(9)
captureError := func(err error) {
ceMutex.Lock()
defer ceMutex.Unlock()
if err != nil {
reterr = err
}
}
go func() {
schemas, err := extractComponentValues[*base.SchemaProxy](ctx, SchemasLabel, root, idx)
captureError(err)
co.Schemas = schemas
wg.Done()
}()
go func() {
parameters, err := extractComponentValues[*Parameter](ctx, ParametersLabel, root, idx)
captureError(err)
co.Parameters = parameters
wg.Done()
}()
go func() {
responses, err := extractComponentValues[*Response](ctx, ResponsesLabel, root, idx)
captureError(err)
co.Responses = responses
wg.Done()
}()
go func() {
examples, err := extractComponentValues[*base.Example](ctx, base.ExamplesLabel, root, idx)
captureError(err)
co.Examples = examples
wg.Done()
}()
go func() {
requestBodies, err := extractComponentValues[*RequestBody](ctx, RequestBodiesLabel, root, idx)
captureError(err)
co.RequestBodies = requestBodies
wg.Done()
}()
go func() {
headers, err := extractComponentValues[*Header](ctx, HeadersLabel, root, idx)
captureError(err)
co.Headers = headers
wg.Done()
}()
go func() {
securitySchemes, err := extractComponentValues[*SecurityScheme](ctx, SecuritySchemesLabel, root, idx)
captureError(err)
co.SecuritySchemes = securitySchemes
wg.Done()
}()
go func() {
links, err := extractComponentValues[*Link](ctx, LinksLabel, root, idx)
captureError(err)
co.Links = links
wg.Done()
}()
go func() {
callbacks, err := extractComponentValues[*Callback](ctx, CallbacksLabel, root, idx)
captureError(err)
co.Callbacks = callbacks
wg.Done()
}()
wg.Wait()
return reterr
}
// extractComponentValues converts all the YAML nodes of a component type to
// low level model.
// Process each node in parallel.
func extractComponentValues[T low.Buildable[N], N any](ctx context.Context, label string, root *yaml.Node, idx *index.SpecIndex) (low.NodeReference[map[low.KeyReference[string]]low.ValueReference[T]], error) {
var emptyResult low.NodeReference[map[low.KeyReference[string]]low.ValueReference[T]]
_, nodeLabel, nodeValue := utils.FindKeyNodeFullTop(label, root.Content)
if nodeValue == nil {
return emptyResult, nil
}
componentValues := make(map[low.KeyReference[string]]low.ValueReference[T])
if utils.IsNodeArray(nodeValue) {
return emptyResult, fmt.Errorf("node is array, cannot be used in components: line %d, column %d", nodeValue.Line, nodeValue.Column)
}
in := make(chan componentInput)
out := make(chan componentBuildResult[T])
done := make(chan struct{})
var wg sync.WaitGroup
wg.Add(2) // input and output goroutines.
// Send input.
go func() {
defer func() {
close(in)
wg.Done()
}()
var currentLabel *yaml.Node
for i, node := range nodeValue.Content {
// always ignore extensions
if i%2 == 0 {
currentLabel = node
continue
}
// only check for lowercase extensions as 'X-' is still valid as a key (annoyingly).
if strings.HasPrefix(currentLabel.Value, "x-") {
continue
}
select {
case in <- componentInput{
node: node,
currentLabel: currentLabel,
}:
case <-done:
return
}
}
}()
// Collect output.
go func() {
for result := range out {
componentValues[result.key] = result.value
}
close(done)
wg.Done()
}()
// Translate.
translateFunc := func(value componentInput) (componentBuildResult[T], error) {
var n T = new(N)
currentLabel := value.currentLabel
node := value.node
// if this is a reference, extract it (although components with references is an antipattern)
// If you're building components as references... pls... stop, this code should not need to be here.
// TODO: check circular crazy on this. It may explode
var err error
if h, _, _ := utils.IsNodeRefValue(node); h && label != SchemasLabel {
node, _, err = low.LocateRefNode(node, idx)
}
if err != nil {
return componentBuildResult[T]{}, err
}
// build.
_ = low.BuildModel(node, n)
err = n.Build(ctx, currentLabel, node, idx)
if err != nil {
return componentBuildResult[T]{}, err
}
return componentBuildResult[T]{
key: low.KeyReference[string]{
KeyNode: currentLabel,
Value: currentLabel.Value,
},
value: low.ValueReference[T]{
Value: n,
ValueNode: node,
},
}, nil
}
err := datamodel.TranslatePipeline[componentInput, componentBuildResult[T]](in, out, translateFunc)
wg.Wait()
if err != nil {
return emptyResult, err
}
results := low.NodeReference[map[low.KeyReference[string]]low.ValueReference[T]]{
KeyNode: nodeLabel,
ValueNode: nodeValue,
Value: componentValues,
}
return results, nil
}