diff --git a/README.md b/README.md index 4a72301..2e1c98c 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ See all the documentation at https://pb33f.io/libopenapi/ - [Using OpenAPI](https://pb33f.io/libopenapi/openapi/) - [Using Swagger](https://pb33f.io/libopenapi/swagger/) - [The Data Model](https://pb33f.io/libopenapi/model/) +- [Modifying / Mutating the OpenAPI Model](https://pb33f.io/libopenapi/modifying/) - [Using Vendor Extensions](https://pb33f.io/libopenapi/extensions/) - [The Index](https://pb33f.io/libopenapi/index/) - [The Resolver](https://pb33f.io/libopenapi/resolver/) diff --git a/datamodel/high/base/schema_proxy.go b/datamodel/high/base/schema_proxy.go index 1d81512..3205bcf 100644 --- a/datamodel/high/base/schema_proxy.go +++ b/datamodel/high/base/schema_proxy.go @@ -48,6 +48,7 @@ type SchemaProxy struct { schema *low.NodeReference[*base.SchemaProxy] buildError error rendered *Schema + refStr string } // NewSchemaProxy creates a new high-level SchemaProxy from a low-level one. @@ -55,6 +56,18 @@ func NewSchemaProxy(schema *low.NodeReference[*base.SchemaProxy]) *SchemaProxy { return &SchemaProxy{schema: schema} } +// CreateSchemaProxy will create a new high-level SchemaProxy from a high-level Schema, this acts the same +// as if the SchemaProxy is pre-rendered. +func CreateSchemaProxy(schema *Schema) *SchemaProxy { + return &SchemaProxy{rendered: schema} +} + +// CreateSchemaProxyRef will create a new high-level SchemaProxy from a reference string, this is used only when +// building out new models from scratch that require a reference rather than a schema implementation. +func CreateSchemaProxyRef(ref string) *SchemaProxy { + return &SchemaProxy{refStr: ref} +} + // Schema will create a new Schema instance using NewSchema from the low-level SchemaProxy backing this high-level one. // If there is a problem building the Schema, then this method will return nil. Use GetBuildError to gain access // to that building error. @@ -76,11 +89,20 @@ func (sp *SchemaProxy) Schema() *Schema { // IsReference returns true if the SchemaProxy is a reference to another Schema. func (sp *SchemaProxy) IsReference() bool { - return sp.schema.Value.IsSchemaReference() + if sp.refStr != "" { + return true + } + if sp.schema != nil { + return sp.schema.Value.IsSchemaReference() + } + return false } // GetReference returns the location of the $ref if this SchemaProxy is a reference to another Schema. func (sp *SchemaProxy) GetReference() string { + if sp.refStr != "" { + return sp.refStr + } return sp.schema.Value.GetSchemaReference() } diff --git a/datamodel/high/base/schema_proxy_test.go b/datamodel/high/base/schema_proxy_test.go index 75f9673..1b9ee7a 100644 --- a/datamodel/high/base/schema_proxy_test.go +++ b/datamodel/high/base/schema_proxy_test.go @@ -31,7 +31,7 @@ func TestSchemaProxy_MarshalYAML(t *testing.T) { var idxNode yaml.Node err := yaml.Unmarshal([]byte(ymlComponents), &idxNode) assert.NoError(t, err) - return index.NewSpecIndex(&idxNode) + return index.NewSpecIndexWithConfig(&idxNode, index.CreateOpenAPIIndexConfig()) }() const ref = "#/components/schemas/nice" @@ -53,3 +53,14 @@ func TestSchemaProxy_MarshalYAML(t *testing.T) { assert.Equal(t, "$ref: '#/components/schemas/nice'", strings.TrimSpace(string(rend))) } + +func TestCreateSchemaProxy(t *testing.T) { + sp := CreateSchemaProxy(&Schema{Description: "iAmASchema"}) + assert.Equal(t, "iAmASchema", sp.rendered.Description) +} + +func TestCreateSchemaProxyRef(t *testing.T) { + sp := CreateSchemaProxyRef("#/components/schemas/MySchema") + assert.Equal(t, "#/components/schemas/MySchema", sp.GetReference()) +} + diff --git a/datamodel/high/node_builder.go b/datamodel/high/node_builder.go index 3e556f3..74ecbbc 100644 --- a/datamodel/high/node_builder.go +++ b/datamodel/high/node_builder.go @@ -474,15 +474,16 @@ func (n *NodeBuilder) AddYAMLNode(parent *yaml.Node, entry *NodeEntry) *yaml.Nod case reflect.Ptr: if r, ok := value.(Renderable); ok { if gl, lg := value.(GoesLowUntyped); lg { - - ut := reflect.ValueOf(gl.GoLowUntyped()) - if !ut.IsNil() { - if gl.GoLowUntyped().(low.IsReferenced).IsReference() { - rvn := utils.CreateEmptyMapNode() - rvn.Content = append(rvn.Content, utils.CreateStringNode("$ref")) - rvn.Content = append(rvn.Content, utils.CreateStringNode(gl.GoLowUntyped().(low.IsReferenced).GetReference())) - valueNode = rvn - break + if gl.GoLowUntyped() != nil { + ut := reflect.ValueOf(gl.GoLowUntyped()) + if !ut.IsNil() { + if gl.GoLowUntyped().(low.IsReferenced).IsReference() { + rvn := utils.CreateEmptyMapNode() + rvn.Content = append(rvn.Content, utils.CreateStringNode("$ref")) + rvn.Content = append(rvn.Content, utils.CreateStringNode(gl.GoLowUntyped().(low.IsReferenced).GetReference())) + valueNode = rvn + break + } } } } @@ -600,6 +601,7 @@ func (n *NodeBuilder) extractLowMapKeys(fg reflect.Value, x string, found bool, return found, orderedCollection } +// Renderable is an interface that can be implemented by types that provide a custom MarshaYAML method. type Renderable interface { MarshalYAML() (interface{}, error) } diff --git a/document_examples_test.go b/document_examples_test.go index 29d9cf2..e89df79 100644 --- a/document_examples_test.go +++ b/document_examples_test.go @@ -8,10 +8,12 @@ import ( "github.com/pb33f/libopenapi/datamodel" "io/ioutil" "net/url" + "os" "strings" "testing" "github.com/pb33f/libopenapi/datamodel/high" + v3high "github.com/pb33f/libopenapi/datamodel/high/v3" low "github.com/pb33f/libopenapi/datamodel/low/base" v3 "github.com/pb33f/libopenapi/datamodel/low/v3" "github.com/pb33f/libopenapi/resolver" @@ -599,3 +601,69 @@ components: // Output: schemaOne 'x-custom-cakes' (some cakes) has 2 cakes, 'someCake' has 10 candles and blue frosting //parameterOne 'x-custom-burgers' (some burgers) has 2 burgers, 'anotherBurger' has mayo sauce and a lamb patty } + +func ExampleNewDocument_modifyAndReRender() { + + // How to read in an OpenAPI 3 Specification, into a Document, + // modify the document and then re-render it back to YAML bytes. + + // load an OpenAPI 3 specification from bytes + petstore, _ := os.ReadFile("test_specs/petstorev3.json") + + // create a new document from specification bytes + doc, err := NewDocument(petstore) + + // if anything went wrong, an error is thrown + if err != nil { + panic(fmt.Sprintf("cannot create new document: %e", err)) + } + + // because we know this is a v3 spec, we can build a ready to go model from it. + v3Model, errors := doc.BuildV3Model() + + // if anything went wrong when building the v3 model, a slice of errors will be returned + if len(errors) > 0 { + for i := range errors { + fmt.Printf("error: %e\n", errors[i]) + } + panic(fmt.Sprintf("cannot create v3 model from document: %d errors reported", len(errors))) + } + + // create a new path item and operation. + newPath := &v3high.PathItem{ + Description: "this is a new path item", + Get: &v3high.Operation{ + Description: "this is a get operation", + OperationId: "getNewThing", + RequestBody: &v3high.RequestBody{ + Description: "this is a new request body", + }, + }, + } + + // capture original number of paths + originalPaths := len(v3Model.Model.Paths.PathItems) + + // add the path to the document + v3Model.Model.Paths.PathItems["/new/path"] = newPath + + // render out the new path item to YAML + // renderedPathItem, _ := yaml.Marshal(newPath) + + // render the document back to bytes and reload the model. + rawBytes, _, newModel, errs := doc.RenderAndReload() + + // if anything went wrong when re-rendering the v3 model, a slice of errors will be returned + if len(errors) > 0 { + panic(fmt.Sprintf("cannot re-render document: %d errors reported", len(errs))) + } + + // capture new number of paths after re-rendering + newPaths := len(newModel.Model.Paths.PathItems) + + // print the number of paths and schemas in the document + fmt.Printf("There were %d original paths. There are now %d paths in the document\n", originalPaths, newPaths) + fmt.Printf("The original spec had %d bytes, the new one has %d\n", len(petstore), len(rawBytes)) + // Output: There were 13 original paths. There are now 14 paths in the document + //The original spec had 31143 bytes, the new one has 27857 +}