-
-
Notifications
You must be signed in to change notification settings - Fork 65
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for different bundling modes #350
base: main
Are you sure you want to change the base?
Changes from all commits
97d3a12
da54f7e
dd5791d
72eb400
7d2618b
62fb3ae
80d86bd
77f5090
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,17 +6,35 @@ package bundler | |
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"strings" | ||
"sync" | ||
|
||
"github.com/davecgh/go-spew/spew" | ||
"github.com/pb33f/libopenapi" | ||
"github.com/pb33f/libopenapi/datamodel" | ||
v3 "github.com/pb33f/libopenapi/datamodel/high/v3" | ||
highV3 "github.com/pb33f/libopenapi/datamodel/high/v3" | ||
"github.com/pb33f/libopenapi/datamodel/low" | ||
"github.com/pb33f/libopenapi/datamodel/low/base" | ||
lowV3 "github.com/pb33f/libopenapi/datamodel/low/v3" | ||
"github.com/pb33f/libopenapi/index" | ||
"gopkg.in/yaml.v3" | ||
) | ||
|
||
// ErrInvalidModel is returned when the model is not usable. | ||
var ErrInvalidModel = errors.New("invalid model") | ||
|
||
type RefHandling string | ||
|
||
const ( | ||
RefHandlingInline RefHandling = "inline" | ||
RefHandlingCompose RefHandling = "compose" | ||
) | ||
|
||
type BundleOptions struct { | ||
RelativeRefHandling RefHandling | ||
} | ||
|
||
// BundleBytes will take a byte slice of an OpenAPI specification and return a bundled version of it. | ||
// This is useful for when you want to take a specification with external references, and you want to bundle it | ||
// into a single document. | ||
|
@@ -25,7 +43,7 @@ var ErrInvalidModel = errors.New("invalid model") | |
// document will be a valid OpenAPI specification, containing no references. | ||
// | ||
// Circular references will not be resolved and will be skipped. | ||
func BundleBytes(bytes []byte, configuration *datamodel.DocumentConfiguration) ([]byte, error) { | ||
func BundleBytes(bytes []byte, configuration *datamodel.DocumentConfiguration, opts BundleOptions) ([]byte, error) { | ||
doc, err := libopenapi.NewDocumentWithConfiguration(bytes, configuration) | ||
if err != nil { | ||
return nil, err | ||
|
@@ -37,7 +55,12 @@ func BundleBytes(bytes []byte, configuration *datamodel.DocumentConfiguration) ( | |
return nil, errors.Join(ErrInvalidModel, err) | ||
} | ||
|
||
bundledBytes, e := bundle(&v3Doc.Model, configuration.BundleInlineRefs) | ||
// Overwrite bundle options, if deprecated config field is used. | ||
if configuration.BundleInlineRefs { | ||
opts.RelativeRefHandling = RefHandlingInline | ||
} | ||
|
||
bundledBytes, e := bundle(&v3Doc.Model, opts) | ||
return bundledBytes, errors.Join(err, e) | ||
} | ||
|
||
|
@@ -50,49 +73,144 @@ func BundleBytes(bytes []byte, configuration *datamodel.DocumentConfiguration) ( | |
// document will be a valid OpenAPI specification, containing no references. | ||
// | ||
// Circular references will not be resolved and will be skipped. | ||
func BundleDocument(model *v3.Document) ([]byte, error) { | ||
return bundle(model, false) | ||
func BundleDocument(model *highV3.Document) ([]byte, error) { | ||
return bundle(model, BundleOptions{RelativeRefHandling: RefHandlingInline}) | ||
} | ||
|
||
func bundle(model *v3.Document, inline bool) ([]byte, error) { | ||
func bundle(model *highV3.Document, opts BundleOptions) (_ []byte, err error) { | ||
rolodex := model.Rolodex | ||
|
||
compact := func(idx *index.SpecIndex, root bool) { | ||
mappedReferences := idx.GetMappedReferences() | ||
sequencedReferences := idx.GetRawReferencesSequenced() | ||
for _, sequenced := range sequencedReferences { | ||
mappedReference := mappedReferences[sequenced.FullDefinition] | ||
|
||
// if we're in the root document, don't bundle anything. | ||
refExp := strings.Split(sequenced.FullDefinition, "#/") | ||
if len(refExp) == 2 { | ||
if refExp[0] == sequenced.Index.GetSpecAbsolutePath() || refExp[0] == "" { | ||
if root && !inline { | ||
idx.GetLogger().Debug("[bundler] skipping local root reference", | ||
"ref", sequenced.Definition) | ||
continue | ||
} | ||
} | ||
idx := rolodex.GetRootIndex() | ||
mappedReferences := idx.GetMappedReferences() | ||
sequencedReferences := idx.GetRawReferencesSequenced() | ||
|
||
for _, sequenced := range sequencedReferences { | ||
mappedReference := mappedReferences[sequenced.FullDefinition] | ||
if mappedReference == nil { | ||
return nil, fmt.Errorf("no mapped reference found for: %s", sequenced.FullDefinition) | ||
} | ||
|
||
if mappedReference.DefinitionFile() == idx.GetSpecAbsolutePath() { | ||
// Don't bundle anything that's in the main file. | ||
continue | ||
} | ||
|
||
switch opts.RelativeRefHandling { | ||
case RefHandlingInline: | ||
// Just deal with simple inlining. | ||
sequenced.Node.Content = mappedReference.Node.Content | ||
case RefHandlingCompose: | ||
// Recursively collect all reference targets to be bundled into the root | ||
// file. | ||
bundledComponents := make(map[string]*index.ReferenceNode) | ||
if err := bundleRefTarget(sequenced, mappedReference, bundledComponents, opts); err != nil { | ||
return nil, err | ||
} | ||
|
||
if mappedReference != nil && !mappedReference.Circular { | ||
sequenced.Node.Content = mappedReference.Node.Content | ||
continue | ||
model, err = composeDocument(model, bundledComponents) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
} | ||
|
||
return model.Render() | ||
} | ||
|
||
func composeDocument(model *highV3.Document, comps map[string]*index.ReferenceNode) (*highV3.Document, error) { | ||
lowModel := model.GoLow() | ||
|
||
components := lowModel.Components | ||
|
||
for def, component := range comps { | ||
defParts := strings.Split(def, "/") | ||
// TODO: use constant from low model labels | ||
if len(defParts) != 4 || defParts[1] != lowV3.ComponentsLabel { | ||
return nil, fmt.Errorf("unsupported component section: %s", def) | ||
} | ||
spew.Dump(component) | ||
|
||
if mappedReference != nil && mappedReference.Circular { | ||
if idx.GetLogger() != nil { | ||
idx.GetLogger().Warn("[bundler] skipping circular reference", | ||
"ref", sequenced.FullDefinition) | ||
} | ||
switch defParts[2] { | ||
case "schemas": | ||
key := low.KeyReference[string]{ | ||
Value: defParts[3], | ||
KeyNode: &yaml.Node{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you're creating a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh no. Thanks. I thought I read somewhere that these would be set once you decide to render. This seems rather difficult. |
||
Kind: yaml.ScalarNode, | ||
Style: yaml.TaggedStyle, | ||
Tag: "!!str", | ||
Value: defParts[3], | ||
}, | ||
} | ||
value := low.ValueReference[*base.SchemaProxy]{ | ||
Reference: low.Reference{}, | ||
Value: &base.SchemaProxy{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of creating a https://github.com/pb33f/libopenapi/blob/main/datamodel/low/base/schema_proxy_test.go#L29 For example: var sch SchemaProxy
var myNode yaml.Node
myIndex := // some index some somewhere.
// .. do stuff to the node
ctx := context.WithValue(context.Background(), "key", "value")
err := sch.Build(ctx, &myNode, myNode.Content[0], myIndex) Now you have a ready to go low level To do build a high level one from it? https://github.com/pb33f/libopenapi/blob/main/datamodel/high/base/schema_proxy_test.go#L56C2-L56C31 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice, I'll check that out. |
||
Reference: low.Reference{}, | ||
NodeMap: &low.NodeMap{Nodes: &sync.Map{}}, | ||
}, | ||
ValueNode: &yaml.Node{}, | ||
} | ||
components.Value.Schemas.Value.Set(key, value) | ||
Comment on lines
+135
to
+152
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would appear, I need to put together the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
|
||
default: | ||
return nil, fmt.Errorf("unsupported component type: %s", defParts[2]) | ||
} | ||
} | ||
|
||
return nil, nil | ||
} | ||
|
||
func bundleRefTarget(ref, mappedRef *index.ReferenceNode, bundledComponents map[string]*index.ReferenceNode, opts BundleOptions) error { | ||
idx := ref.Index | ||
if mappedRef == nil { | ||
if idx.GetLogger() != nil { | ||
idx.GetLogger().Warn("[bundler] skipping unresolved reference", | ||
"ref", ref.FullDefinition) | ||
} | ||
return nil | ||
} | ||
|
||
indexes := rolodex.GetIndexes() | ||
for _, idx := range indexes { | ||
compact(idx, false) | ||
if mappedRef.Circular { | ||
if idx.GetLogger() != nil { | ||
idx.GetLogger().Warn("[bundler] skipping circular reference", | ||
"ref", ref.FullDefinition) | ||
} | ||
return nil | ||
} | ||
compact(rolodex.GetRootIndex(), true) | ||
return model.Render() | ||
|
||
bundledRef, exists := bundledComponents[mappedRef.Definition] | ||
if exists && bundledRef.FullDefinition != mappedRef.FullDefinition { | ||
// TODO: we don't want to error here | ||
return fmt.Errorf("duplicate component definition: %s", mappedRef.Definition) | ||
} else { | ||
bundledComponents[mappedRef.Definition] = mappedRef | ||
ref.KeyNode.Value = mappedRef.Definition | ||
} | ||
|
||
// When composing, we need to update the ref values to point to a local reference. At the | ||
// same time we need to track all components referenced by any children of the target, so | ||
// that we can include them in the final document. | ||
// | ||
// One issue we might face is that the name of a target component in any given target | ||
// document is the same as that of another component in a different target document or | ||
// even the root document. | ||
|
||
// Obtain the target's file's index because we should find child references using that. | ||
// Otherwise ExtractRefs will use the ref's index and it's absolute spec path for | ||
// the FullPath of any extracted ref targets. | ||
targetIndex := idx | ||
if targetFile := mappedRef.DefinitionFile(); targetFile != "" { | ||
targetIndex = idx.GetRolodex().GetFileIndex(targetFile) | ||
} | ||
|
||
targetMappedReferences := targetIndex.GetMappedReferences() | ||
|
||
childRefs := targetIndex.ExtractRefs(mappedRef.Node, mappedRef.ParentNode, make([]string, 0), 0, false, "") | ||
for _, childRef := range childRefs { | ||
childRefTarget := targetMappedReferences[childRef.FullDefinition] | ||
if err := bundleRefTarget(childRef, childRefTarget, bundledComponents, opts); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made a tiny bit of progress today. The idea is that
bundledComponents
now contains all theyaml.Node
s which are to be bundled into the root spec, using theirDefinition
(i.e.#/components/schemas/MySchema
) as keys.What I'm stuck with now, is that I don't know how to actually use these nodes. I figured the "right" way, you mentioned in your previous comments, would be to manipulate the
low
model by adding to the components there. I just can't make sense of the types used there. See my next comment.