diff --git a/datamodel/high/node_builder.go b/datamodel/high/node_builder.go index 5aa1537..8afa1ec 100644 --- a/datamodel/high/node_builder.go +++ b/datamodel/high/node_builder.go @@ -22,6 +22,7 @@ type NodeEntry struct { Value any StringValue string Line int + Style yaml.Style RenderZero bool } @@ -88,6 +89,7 @@ func (n *NodeBuilder) add(key string, i int) { for k := range originalExtensions { if k.Value == extKey { if originalExtensions[k].ValueNode.Line != 0 { + nodeEntry.Style = originalExtensions[k].ValueNode.Style nodeEntry.Line = originalExtensions[k].ValueNode.Line + u } else { nodeEntry.Line = 999999 + b + u @@ -173,31 +175,43 @@ func (n *NodeBuilder) add(key string, i int) { lowFieldValue := reflect.ValueOf(n.Low).Elem().FieldByName(key) fLow := lowFieldValue.Interface() value = reflect.ValueOf(fLow) + + type lineStyle struct { + line int + style yaml.Style + } + switch value.Kind() { case reflect.Slice: l := value.Len() - lines := make([]int, l) + lines := make([]lineStyle, l) for g := 0; g < l; g++ { qw := value.Index(g).Interface() if we, wok := qw.(low.HasKeyNode); wok { - lines[g] = we.GetKeyNode().Line + lines[g] = lineStyle{we.GetKeyNode().Line, we.GetKeyNode().Style} } } - sort.Ints(lines) - nodeEntry.Line = lines[0] // pick the lowest line number so this key is sorted in order. + sort.Slice(lines, func(i, j int) bool { + return lines[i].line < lines[j].line + }) + nodeEntry.Line = lines[0].line // pick the lowest line number so this key is sorted in order. + nodeEntry.Style = lines[0].style break case reflect.Map: l := value.Len() - lines := make([]int, l) + lines := make([]lineStyle, l) for q, ky := range value.MapKeys() { if we, wok := ky.Interface().(low.HasKeyNode); wok { - lines[q] = we.GetKeyNode().Line + lines[q] = lineStyle{we.GetKeyNode().Line, we.GetKeyNode().Style} } } - sort.Ints(lines) - nodeEntry.Line = lines[0] // pick the lowest line number, sort in order + sort.Slice(lines, func(i, j int) bool { + return lines[i].line < lines[j].line + }) + nodeEntry.Line = lines[0].line // pick the lowest line number, sort in order + nodeEntry.Style = lines[0].style case reflect.Struct: y := value.Interface() @@ -206,11 +220,13 @@ func (n *NodeBuilder) add(key string, i int) { if nb.IsReference() { if jk, kj := y.(low.HasKeyNode); kj { nodeEntry.Line = jk.GetKeyNode().Line + nodeEntry.Style = jk.GetKeyNode().Style break } } if nb.GetValueNode() != nil { nodeEntry.Line = nb.GetValueNode().Line + nodeEntry.Style = nb.GetValueNode().Style } } default: @@ -290,6 +306,7 @@ func (n *NodeBuilder) AddYAMLNode(parent *yaml.Node, entry *NodeEntry) *yaml.Nod val := value.(string) valueNode = utils.CreateStringNode(val) valueNode.Line = line + valueNode.Style = entry.Style break case reflect.Bool: diff --git a/datamodel/high/v3/document.go b/datamodel/high/v3/document.go index 29526b6..ffa125c 100644 --- a/datamodel/high/v3/document.go +++ b/datamodel/high/v3/document.go @@ -10,6 +10,7 @@ package v3 import ( + "bytes" "github.com/pb33f/libopenapi/datamodel/high" "github.com/pb33f/libopenapi/datamodel/high/base" low "github.com/pb33f/libopenapi/datamodel/low/v3" @@ -154,13 +155,26 @@ func (d *Document) Render() ([]byte, error) { return yaml.Marshal(d) } +// RenderWithIndention will return a YAML representation of the Document object as a byte slice. +// the rendering will use the original indention of the document. +func (d *Document) RenderWithIndention(indent int) ([]byte, error) { + var buf bytes.Buffer + yamlEncoder := yaml.NewEncoder(&buf) + yamlEncoder.SetIndent(indent) + err := yamlEncoder.Encode(d) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + // RenderJSON will return a JSON representation of the Document object as a byte slice. -func (d *Document) RenderJSON() ([]byte, error) { +func (d *Document) RenderJSON(indention string) ([]byte, error) { yamlData, err := yaml.Marshal(d) if err != nil { return yamlData, err } - return utils.ConvertYAMLtoJSONPretty(yamlData, "", " ") + return utils.ConvertYAMLtoJSONPretty(yamlData, "", indention) } func (d *Document) RenderInline() ([]byte, error) { diff --git a/datamodel/high/v3/media_type_test.go b/datamodel/high/v3/media_type_test.go index 6816c7c..157342f 100644 --- a/datamodel/high/v3/media_type_test.go +++ b/datamodel/high/v3/media_type_test.go @@ -46,58 +46,58 @@ func TestMediaType_MarshalYAMLInline(t *testing.T) { required: - name - photoUrls - type: object + type: "object" properties: id: - type: integer - format: int64 + type: "integer" + format: "int64" example: 10 name: - type: string - example: doggie + type: "string" + example: "doggie" category: - type: object + type: "object" properties: id: - type: integer - format: int64 + type: "integer" + format: "int64" example: 1 name: - type: string - example: Dogs + type: "string" + example: "Dogs" xml: - name: category + name: "category" photoUrls: - type: array + type: "array" xml: wrapped: true items: - type: string + type: "string" xml: - name: photoUrl + name: "photoUrl" tags: - type: array + type: "array" xml: wrapped: true items: - type: object + type: "object" properties: id: - type: integer - format: int64 + type: "integer" + format: "int64" name: - type: string + type: "string" xml: - name: tag + name: "tag" status: - type: string - description: pet status in the store + type: "string" + description: "pet status in the store" enum: - available - pending - sold xml: - name: pet + name: "pet" example: testing a nice mutation` yml, _ = mt.RenderInline() diff --git a/datamodel/spec_info.go b/datamodel/spec_info.go index cb39e68..8eb07e4 100644 --- a/datamodel/spec_info.go +++ b/datamodel/spec_info.go @@ -22,18 +22,19 @@ const ( // SpecInfo represents a 'ready-to-process' OpenAPI Document. The RootNode is the most important property // used by the library, this contains the top of the document tree that every single low model is based off. type SpecInfo struct { - SpecType string `json:"type"` - Version string `json:"version"` - SpecFormat string `json:"format"` - SpecFileType string `json:"fileType"` - SpecBytes *[]byte `json:"bytes"` // the original byte array - RootNode *yaml.Node `json:"-"` // reference to the root node of the spec. - SpecJSONBytes *[]byte `json:"-"` // original bytes converted to JSON - SpecJSON *map[string]interface{} `json:"-"` // standard JSON map of original bytes - Error error `json:"-"` // something go wrong? - APISchema string `json:"-"` // API Schema for supplied spec type (2 or 3) - Generated time.Time `json:"-"` - JsonParsingChannel chan bool `json:"-"` + SpecType string `json:"type"` + Version string `json:"version"` + SpecFormat string `json:"format"` + SpecFileType string `json:"fileType"` + SpecBytes *[]byte `json:"bytes"` // the original byte array + RootNode *yaml.Node `json:"-"` // reference to the root node of the spec. + SpecJSONBytes *[]byte `json:"-"` // original bytes converted to JSON + SpecJSON *map[string]interface{} `json:"-"` // standard JSON map of original bytes + Error error `json:"-"` // something go wrong? + APISchema string `json:"-"` // API Schema for supplied spec type (2 or 3) + Generated time.Time `json:"-"` + JsonParsingChannel chan bool `json:"-"` + OriginalIndentation int `json:"-"` // the original whitespace } // GetJSONParsingChannel returns a channel that will close once async JSON parsing is completed. @@ -177,6 +178,9 @@ func ExtractSpecInfo(spec []byte) (*SpecInfo, error) { return specVersion, specVersion.Error } + // detect the original whitespace indentation + specVersion.OriginalIndentation = utils.DetermineWhitespaceLength(string(spec)) + return specVersion, nil } diff --git a/document.go b/document.go index 67edf7d..bbc0489 100644 --- a/document.go +++ b/document.go @@ -167,10 +167,17 @@ func (d *document) RenderAndReload() ([]byte, Document, *DocumentModel[v3high.Do // render the model as the correct type based on the source. // https://github.com/pb33f/libopenapi/issues/105 if d.info.SpecFileType == datamodel.JSONFileType { - newBytes, renderError = d.highOpenAPI3Model.Model.RenderJSON() + jsonIndent := " " + i := d.info.OriginalIndentation + if i > 2 { + for l := 0; l < i-2; l++ { + jsonIndent += " " + } + } + newBytes, renderError = d.highOpenAPI3Model.Model.RenderJSON(jsonIndent) } if d.info.SpecFileType == datamodel.YAMLFileType { - newBytes, renderError = d.highOpenAPI3Model.Model.Render() + newBytes, renderError = d.highOpenAPI3Model.Model.RenderWithIndention(d.info.OriginalIndentation) } if renderError != nil { diff --git a/document_test.go b/document_test.go index 0d74aba..f2f6620 100644 --- a/document_test.go +++ b/document_test.go @@ -622,3 +622,54 @@ func TestDocument_InputAsJSON(t *testing.T) { assert.Equal(t, d, strings.TrimSpace(string(rend))) } + +func TestDocument_InputAsJSON_LargeIndent(t *testing.T) { + + var d = `{ + "openapi": "3.1", + "paths": { + "/an/operation": { + "get": { + "operationId": "thisIsAnOperationId" + } + } + } +}` + + doc, err := NewDocumentWithConfiguration([]byte(d), datamodel.NewOpenDocumentConfiguration()) + if err != nil { + panic(err) + } + + _, _ = doc.BuildV3Model() + + // render the document. + rend, _, _, _ := doc.RenderAndReload() + + assert.Equal(t, d, strings.TrimSpace(string(rend))) +} + +func TestDocument_RenderWithIndention(t *testing.T) { + + spec := `openapi: "3.1.0" +info: + title: Test + version: 1.0.0 +paths: + /test: + get: + operationId: 'test'` + + config := datamodel.NewOpenDocumentConfiguration() + + doc, err := NewDocumentWithConfiguration([]byte(spec), config) + if err != nil { + panic(err) + } + + _, _ = doc.BuildV3Model() + + rend, _, _, _ := doc.RenderAndReload() + + assert.Equal(t, spec, strings.TrimSpace(string(rend))) +} diff --git a/utils/utils.go b/utils/utils.go index cbfe183..dd80b8e 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -5,6 +5,7 @@ import ( "fmt" "net/url" "regexp" + "sort" "strconv" "strings" @@ -674,3 +675,19 @@ func CheckEnumForDuplicates(seq []*yaml.Node) []*yaml.Node { } return res } + +// DetermineWhitespaceLength will determine the length of the whitespace for a JSON or YAML file. +func DetermineWhitespaceLength(input string) int { + exp := regexp.MustCompile(`\n( +)`) + whiteSpace := exp.FindAllStringSubmatch(input, -1) + var filtered []string + for i := range whiteSpace { + filtered = append(filtered, whiteSpace[i][1]) + } + sort.Strings(filtered) + if len(filtered) > 0 { + return len(filtered[0]) + } else { + return 0 + } +} diff --git a/utils/utils_test.go b/utils/utils_test.go index 7d3b176..f30fba5 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -3,7 +3,7 @@ package utils import ( "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" - "io/ioutil" + "os" "sync" "testing" ) @@ -18,7 +18,7 @@ var ( func getPetstore() petstore { once.Do(func() { - psBytes, _ = ioutil.ReadFile("../test_specs/petstorev3.json") + psBytes, _ = os.ReadFile("../test_specs/petstorev3.json") }) return psBytes } @@ -785,7 +785,7 @@ func TestIsNodeRefValue_False(t *testing.T) { func TestCheckEnumForDuplicates_Success(t *testing.T) { yml := "- yes\n- no\n- crisps" var rootNode yaml.Node - yaml.Unmarshal([]byte(yml), &rootNode) + _ = yaml.Unmarshal([]byte(yml), &rootNode) assert.Len(t, CheckEnumForDuplicates(rootNode.Content[0].Content), 0) } @@ -793,7 +793,7 @@ func TestCheckEnumForDuplicates_Success(t *testing.T) { func TestCheckEnumForDuplicates_Fail(t *testing.T) { yml := "- yes\n- no\n- crisps\n- no" var rootNode yaml.Node - yaml.Unmarshal([]byte(yml), &rootNode) + _ = yaml.Unmarshal([]byte(yml), &rootNode) assert.Len(t, CheckEnumForDuplicates(rootNode.Content[0].Content), 1) } @@ -802,7 +802,7 @@ func TestCheckEnumForDuplicates_FailMultiple(t *testing.T) { yml := "- yes\n- no\n- crisps\n- no\n- rice\n- yes\n- no" var rootNode yaml.Node - yaml.Unmarshal([]byte(yml), &rootNode) + _ = yaml.Unmarshal([]byte(yml), &rootNode) assert.Len(t, CheckEnumForDuplicates(rootNode.Content[0].Content), 3) } @@ -811,3 +811,13 @@ func TestConvertComponentIdIntoFriendlyPathSearch_Brackets(t *testing.T) { assert.Equal(t, "$.components.schemas['OhNoWhy[HaveYouDoneThis]']", path) assert.Equal(t, "OhNoWhy[HaveYouDoneThis]", segment) } + +func TestDetermineYAMLWhitespaceLength(t *testing.T) { + someBytes, _ := os.ReadFile("../test_specs/burgershop.openapi.yaml") + assert.Equal(t, 2, DetermineWhitespaceLength(string(someBytes))) +} + +func TestDetermineJSONWhitespaceLength(t *testing.T) { + someBytes, _ := os.ReadFile("../test_specs/petstorev3.json") + assert.Equal(t, 2, DetermineWhitespaceLength(string(someBytes))) +}