mirror of
https://github.com/LukeHagar/libopenapi.git
synced 2025-12-06 04:20:11 +00:00
309 lines
8.8 KiB
Go
309 lines
8.8 KiB
Go
// Copyright 2022-2023 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/index"
|
|
"github.com/pb33f/libopenapi/orderedmap"
|
|
"github.com/pb33f/libopenapi/utils"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// PathItem represents a low-level OpenAPI 3+ PathItem object.
|
|
//
|
|
// Describes the operations available on a single path. A Path Item MAY be empty, due to ACL constraints.
|
|
// The path itself is still exposed to the documentation viewer, but they will not know which operations and parameters
|
|
// are available.
|
|
// - https://spec.openapis.org/oas/v3.1.0#path-item-object
|
|
type PathItem struct {
|
|
Description low.NodeReference[string]
|
|
Summary low.NodeReference[string]
|
|
Get low.NodeReference[*Operation]
|
|
Put low.NodeReference[*Operation]
|
|
Post low.NodeReference[*Operation]
|
|
Delete low.NodeReference[*Operation]
|
|
Options low.NodeReference[*Operation]
|
|
Head low.NodeReference[*Operation]
|
|
Patch low.NodeReference[*Operation]
|
|
Trace low.NodeReference[*Operation]
|
|
Servers low.NodeReference[[]low.ValueReference[*Server]]
|
|
Parameters low.NodeReference[[]low.ValueReference[*Parameter]]
|
|
Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]]
|
|
KeyNode *yaml.Node
|
|
RootNode *yaml.Node
|
|
*low.Reference
|
|
low.NodeMap
|
|
}
|
|
|
|
// Hash will return a consistent SHA256 Hash of the PathItem object
|
|
func (p *PathItem) Hash() [32]byte {
|
|
var f []string
|
|
if !p.Description.IsEmpty() {
|
|
f = append(f, p.Description.Value)
|
|
}
|
|
if !p.Summary.IsEmpty() {
|
|
f = append(f, p.Summary.Value)
|
|
}
|
|
if !p.Get.IsEmpty() {
|
|
f = append(f, fmt.Sprintf("%s-%s", GetLabel, low.GenerateHashString(p.Get.Value)))
|
|
}
|
|
if !p.Put.IsEmpty() {
|
|
f = append(f, fmt.Sprintf("%s-%s", PutLabel, low.GenerateHashString(p.Put.Value)))
|
|
}
|
|
if !p.Post.IsEmpty() {
|
|
f = append(f, fmt.Sprintf("%s-%s", PutLabel, low.GenerateHashString(p.Post.Value)))
|
|
}
|
|
if !p.Delete.IsEmpty() {
|
|
f = append(f, fmt.Sprintf("%s-%s", DeleteLabel, low.GenerateHashString(p.Delete.Value)))
|
|
}
|
|
if !p.Options.IsEmpty() {
|
|
f = append(f, fmt.Sprintf("%s-%s", OptionsLabel, low.GenerateHashString(p.Options.Value)))
|
|
}
|
|
if !p.Head.IsEmpty() {
|
|
f = append(f, fmt.Sprintf("%s-%s", HeadLabel, low.GenerateHashString(p.Head.Value)))
|
|
}
|
|
if !p.Patch.IsEmpty() {
|
|
f = append(f, fmt.Sprintf("%s-%s", PatchLabel, low.GenerateHashString(p.Patch.Value)))
|
|
}
|
|
if !p.Trace.IsEmpty() {
|
|
f = append(f, fmt.Sprintf("%s-%s", TraceLabel, low.GenerateHashString(p.Trace.Value)))
|
|
}
|
|
keys := make([]string, len(p.Parameters.Value))
|
|
for k := range p.Parameters.Value {
|
|
keys[k] = low.GenerateHashString(p.Parameters.Value[k].Value)
|
|
}
|
|
sort.Strings(keys)
|
|
f = append(f, keys...)
|
|
keys = make([]string, len(p.Servers.Value))
|
|
for k := range p.Servers.Value {
|
|
keys[k] = low.GenerateHashString(p.Servers.Value[k].Value)
|
|
}
|
|
sort.Strings(keys)
|
|
f = append(f, keys...)
|
|
f = append(f, low.HashExtensions(p.Extensions)...)
|
|
return sha256.Sum256([]byte(strings.Join(f, "|")))
|
|
}
|
|
|
|
// GetRootNode returns the root yaml node of the PathItem object
|
|
func (p *PathItem) GetRootNode() *yaml.Node {
|
|
return p.RootNode
|
|
}
|
|
|
|
// GetKeyNode returns the key yaml node of the PathItem object
|
|
func (p *PathItem) GetKeyNode() *yaml.Node {
|
|
return p.KeyNode
|
|
}
|
|
|
|
// FindExtension attempts to find an extension
|
|
func (p *PathItem) FindExtension(ext string) *low.ValueReference[*yaml.Node] {
|
|
return low.FindItemInOrderedMap(ext, p.Extensions)
|
|
}
|
|
|
|
// GetExtensions returns all PathItem extensions and satisfies the low.HasExtensions interface.
|
|
func (p *PathItem) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] {
|
|
return p.Extensions
|
|
}
|
|
|
|
// Build extracts extensions, parameters, servers and each http method defined.
|
|
// everything is extracted asynchronously for speed.
|
|
func (p *PathItem) Build(ctx context.Context, keyNode, root *yaml.Node, idx *index.SpecIndex) error {
|
|
root = utils.NodeAlias(root)
|
|
p.KeyNode = keyNode
|
|
p.RootNode = root
|
|
utils.CheckForMergeNodes(root)
|
|
p.Reference = new(low.Reference)
|
|
p.Nodes = low.ExtractNodes(ctx, root)
|
|
p.Extensions = low.ExtractExtensions(root)
|
|
low.ExtractExtensionNodes(ctx, p.Extensions, p.Nodes)
|
|
skip := false
|
|
var currentNode *yaml.Node
|
|
|
|
var wg sync.WaitGroup
|
|
var errors []error
|
|
var ops []low.NodeReference[*Operation]
|
|
|
|
// extract parameters
|
|
params, ln, vn, pErr := low.ExtractArray[*Parameter](ctx, ParametersLabel, root, idx)
|
|
if pErr != nil {
|
|
return pErr
|
|
}
|
|
if params != nil {
|
|
p.Parameters = low.NodeReference[[]low.ValueReference[*Parameter]]{
|
|
Value: params,
|
|
KeyNode: ln,
|
|
ValueNode: vn,
|
|
}
|
|
p.Nodes.Store(ln.Line, ln)
|
|
}
|
|
|
|
_, ln, vn = utils.FindKeyNodeFullTop(ServersLabel, root.Content)
|
|
if vn != nil {
|
|
if utils.IsNodeArray(vn) {
|
|
var servers []low.ValueReference[*Server]
|
|
for _, srvN := range vn.Content {
|
|
if utils.IsNodeMap(srvN) {
|
|
srvr := new(Server)
|
|
_ = low.BuildModel(srvN, srvr)
|
|
srvr.Build(ctx, ln, srvN, idx)
|
|
servers = append(servers, low.ValueReference[*Server]{
|
|
Value: srvr,
|
|
ValueNode: srvN,
|
|
})
|
|
}
|
|
}
|
|
p.Servers = low.NodeReference[[]low.ValueReference[*Server]]{
|
|
Value: servers,
|
|
KeyNode: ln,
|
|
ValueNode: vn,
|
|
}
|
|
p.Nodes.Store(ln.Line, ln)
|
|
}
|
|
}
|
|
|
|
for i, pathNode := range root.Content {
|
|
if strings.HasPrefix(strings.ToLower(pathNode.Value), "x-") {
|
|
skip = true
|
|
continue
|
|
}
|
|
if strings.HasPrefix(strings.ToLower(pathNode.Value), "parameters") {
|
|
skip = true
|
|
continue
|
|
}
|
|
if skip {
|
|
skip = false
|
|
continue
|
|
}
|
|
if i%2 == 0 {
|
|
currentNode = pathNode
|
|
continue
|
|
}
|
|
|
|
// the only thing we now care about is handling operations, filter out anything that's not a verb.
|
|
switch currentNode.Value {
|
|
case GetLabel:
|
|
case PostLabel:
|
|
case PutLabel:
|
|
case PatchLabel:
|
|
case DeleteLabel:
|
|
case HeadLabel:
|
|
case OptionsLabel:
|
|
case TraceLabel:
|
|
default:
|
|
continue // ignore everything else.
|
|
}
|
|
|
|
foundContext := ctx
|
|
var op Operation
|
|
opIsRef := false
|
|
var opRefVal string
|
|
var opRefNode *yaml.Node
|
|
if ok, _, ref := utils.IsNodeRefValue(pathNode); ok {
|
|
// According to OpenAPI spec the only valid $ref for paths is
|
|
// reference for the whole pathItem. Unfortunately, internet is full of invalid specs
|
|
// even from trusted companies like DigitalOcean where they tend to
|
|
// use file $ref for each respective operation:
|
|
// /endpoint/call/name:
|
|
// post:
|
|
// $ref: 'file.yaml'
|
|
// Check if that is the case and resolve such thing properly too.
|
|
|
|
opIsRef = true
|
|
opRefVal = ref
|
|
opRefNode = pathNode
|
|
r, newIdx, err, nCtx := low.LocateRefNodeWithContext(ctx, pathNode, idx)
|
|
if r != nil {
|
|
if r.Kind == yaml.DocumentNode {
|
|
r = r.Content[0]
|
|
}
|
|
pathNode = r
|
|
foundContext = nCtx
|
|
foundContext = context.WithValue(foundContext, index.FoundIndexKey, newIdx)
|
|
|
|
if r.Tag == "" {
|
|
// If it's a node from file, tag is empty
|
|
pathNode = r.Content[0]
|
|
}
|
|
|
|
if err != nil {
|
|
if !idx.AllowCircularReferenceResolving() {
|
|
return fmt.Errorf("build schema failed: %s", err.Error())
|
|
}
|
|
}
|
|
} else {
|
|
return fmt.Errorf("path item build failed: cannot find reference: %s at line %d, col %d",
|
|
pathNode.Content[1].Value, pathNode.Content[1].Line, pathNode.Content[1].Column)
|
|
}
|
|
} else {
|
|
foundContext = context.WithValue(foundContext, index.FoundIndexKey, idx)
|
|
}
|
|
wg.Add(1)
|
|
low.BuildModelAsync(pathNode, &op, &wg, &errors)
|
|
|
|
opRef := low.NodeReference[*Operation]{
|
|
Value: &op,
|
|
KeyNode: currentNode,
|
|
ValueNode: pathNode,
|
|
Context: foundContext,
|
|
}
|
|
if opIsRef {
|
|
opRef.SetReference(opRefVal, opRefNode)
|
|
}
|
|
|
|
ops = append(ops, opRef)
|
|
|
|
switch currentNode.Value {
|
|
case GetLabel:
|
|
p.Get = opRef
|
|
case PostLabel:
|
|
p.Post = opRef
|
|
case PutLabel:
|
|
p.Put = opRef
|
|
case PatchLabel:
|
|
p.Patch = opRef
|
|
case DeleteLabel:
|
|
p.Delete = opRef
|
|
case HeadLabel:
|
|
p.Head = opRef
|
|
case OptionsLabel:
|
|
p.Options = opRef
|
|
case TraceLabel:
|
|
p.Trace = opRef
|
|
}
|
|
}
|
|
|
|
// all operations have been superficially built,
|
|
// now we need to build out the operation, we will do this asynchronously for speed.
|
|
translateFunc := func(_ int, op low.NodeReference[*Operation]) (any, error) {
|
|
ref := ""
|
|
var refNode *yaml.Node
|
|
if op.IsReference() {
|
|
ref = op.GetReference()
|
|
refNode = op.GetReferenceNode()
|
|
}
|
|
|
|
err := op.Value.Build(op.Context, op.KeyNode, op.ValueNode, op.Context.Value(index.FoundIndexKey).(*index.SpecIndex))
|
|
if ref != "" {
|
|
op.Value.Reference.SetReference(ref, refNode)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return nil, nil
|
|
}
|
|
err := datamodel.TranslateSliceParallel[low.NodeReference[*Operation], any](ops, translateFunc, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|