Original indention and text delimiter style retained when rendering #106

Now the original indention is captured and string delimiters are retained when rendering out documents.

Signed-off-by: Dave Shanley <dave@quobix.com>

# fixes 106
This commit is contained in:
Dave Shanley
2023-06-16 09:50:27 -04:00
committed by quobix
parent 5b128c098a
commit e1f0f69650
8 changed files with 172 additions and 52 deletions

View File

@@ -22,6 +22,7 @@ type NodeEntry struct {
Value any Value any
StringValue string StringValue string
Line int Line int
Style yaml.Style
RenderZero bool RenderZero bool
} }
@@ -88,6 +89,7 @@ func (n *NodeBuilder) add(key string, i int) {
for k := range originalExtensions { for k := range originalExtensions {
if k.Value == extKey { if k.Value == extKey {
if originalExtensions[k].ValueNode.Line != 0 { if originalExtensions[k].ValueNode.Line != 0 {
nodeEntry.Style = originalExtensions[k].ValueNode.Style
nodeEntry.Line = originalExtensions[k].ValueNode.Line + u nodeEntry.Line = originalExtensions[k].ValueNode.Line + u
} else { } else {
nodeEntry.Line = 999999 + b + u 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) lowFieldValue := reflect.ValueOf(n.Low).Elem().FieldByName(key)
fLow := lowFieldValue.Interface() fLow := lowFieldValue.Interface()
value = reflect.ValueOf(fLow) value = reflect.ValueOf(fLow)
type lineStyle struct {
line int
style yaml.Style
}
switch value.Kind() { switch value.Kind() {
case reflect.Slice: case reflect.Slice:
l := value.Len() l := value.Len()
lines := make([]int, l) lines := make([]lineStyle, l)
for g := 0; g < l; g++ { for g := 0; g < l; g++ {
qw := value.Index(g).Interface() qw := value.Index(g).Interface()
if we, wok := qw.(low.HasKeyNode); wok { if we, wok := qw.(low.HasKeyNode); wok {
lines[g] = we.GetKeyNode().Line lines[g] = lineStyle{we.GetKeyNode().Line, we.GetKeyNode().Style}
} }
} }
sort.Ints(lines) sort.Slice(lines, func(i, j int) bool {
nodeEntry.Line = lines[0] // pick the lowest line number so this key is sorted in order. 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 break
case reflect.Map: case reflect.Map:
l := value.Len() l := value.Len()
lines := make([]int, l) lines := make([]lineStyle, l)
for q, ky := range value.MapKeys() { for q, ky := range value.MapKeys() {
if we, wok := ky.Interface().(low.HasKeyNode); wok { 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) sort.Slice(lines, func(i, j int) bool {
nodeEntry.Line = lines[0] // pick the lowest line number, sort in order 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: case reflect.Struct:
y := value.Interface() y := value.Interface()
@@ -206,11 +220,13 @@ func (n *NodeBuilder) add(key string, i int) {
if nb.IsReference() { if nb.IsReference() {
if jk, kj := y.(low.HasKeyNode); kj { if jk, kj := y.(low.HasKeyNode); kj {
nodeEntry.Line = jk.GetKeyNode().Line nodeEntry.Line = jk.GetKeyNode().Line
nodeEntry.Style = jk.GetKeyNode().Style
break break
} }
} }
if nb.GetValueNode() != nil { if nb.GetValueNode() != nil {
nodeEntry.Line = nb.GetValueNode().Line nodeEntry.Line = nb.GetValueNode().Line
nodeEntry.Style = nb.GetValueNode().Style
} }
} }
default: default:
@@ -290,6 +306,7 @@ func (n *NodeBuilder) AddYAMLNode(parent *yaml.Node, entry *NodeEntry) *yaml.Nod
val := value.(string) val := value.(string)
valueNode = utils.CreateStringNode(val) valueNode = utils.CreateStringNode(val)
valueNode.Line = line valueNode.Line = line
valueNode.Style = entry.Style
break break
case reflect.Bool: case reflect.Bool:

View File

@@ -10,6 +10,7 @@
package v3 package v3
import ( import (
"bytes"
"github.com/pb33f/libopenapi/datamodel/high" "github.com/pb33f/libopenapi/datamodel/high"
"github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/libopenapi/datamodel/high/base"
low "github.com/pb33f/libopenapi/datamodel/low/v3" low "github.com/pb33f/libopenapi/datamodel/low/v3"
@@ -154,13 +155,26 @@ func (d *Document) Render() ([]byte, error) {
return yaml.Marshal(d) 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. // 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) yamlData, err := yaml.Marshal(d)
if err != nil { if err != nil {
return yamlData, err return yamlData, err
} }
return utils.ConvertYAMLtoJSONPretty(yamlData, "", " ") return utils.ConvertYAMLtoJSONPretty(yamlData, "", indention)
} }
func (d *Document) RenderInline() ([]byte, error) { func (d *Document) RenderInline() ([]byte, error) {

View File

@@ -46,58 +46,58 @@ func TestMediaType_MarshalYAMLInline(t *testing.T) {
required: required:
- name - name
- photoUrls - photoUrls
type: object type: "object"
properties: properties:
id: id:
type: integer type: "integer"
format: int64 format: "int64"
example: 10 example: 10
name: name:
type: string type: "string"
example: doggie example: "doggie"
category: category:
type: object type: "object"
properties: properties:
id: id:
type: integer type: "integer"
format: int64 format: "int64"
example: 1 example: 1
name: name:
type: string type: "string"
example: Dogs example: "Dogs"
xml: xml:
name: category name: "category"
photoUrls: photoUrls:
type: array type: "array"
xml: xml:
wrapped: true wrapped: true
items: items:
type: string type: "string"
xml: xml:
name: photoUrl name: "photoUrl"
tags: tags:
type: array type: "array"
xml: xml:
wrapped: true wrapped: true
items: items:
type: object type: "object"
properties: properties:
id: id:
type: integer type: "integer"
format: int64 format: "int64"
name: name:
type: string type: "string"
xml: xml:
name: tag name: "tag"
status: status:
type: string type: "string"
description: pet status in the store description: "pet status in the store"
enum: enum:
- available - available
- pending - pending
- sold - sold
xml: xml:
name: pet name: "pet"
example: testing a nice mutation` example: testing a nice mutation`
yml, _ = mt.RenderInline() yml, _ = mt.RenderInline()

View File

@@ -22,18 +22,19 @@ const (
// SpecInfo represents a 'ready-to-process' OpenAPI Document. The RootNode is the most important property // 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. // used by the library, this contains the top of the document tree that every single low model is based off.
type SpecInfo struct { type SpecInfo struct {
SpecType string `json:"type"` SpecType string `json:"type"`
Version string `json:"version"` Version string `json:"version"`
SpecFormat string `json:"format"` SpecFormat string `json:"format"`
SpecFileType string `json:"fileType"` SpecFileType string `json:"fileType"`
SpecBytes *[]byte `json:"bytes"` // the original byte array SpecBytes *[]byte `json:"bytes"` // the original byte array
RootNode *yaml.Node `json:"-"` // reference to the root node of the spec. RootNode *yaml.Node `json:"-"` // reference to the root node of the spec.
SpecJSONBytes *[]byte `json:"-"` // original bytes converted to JSON SpecJSONBytes *[]byte `json:"-"` // original bytes converted to JSON
SpecJSON *map[string]interface{} `json:"-"` // standard JSON map of original bytes SpecJSON *map[string]interface{} `json:"-"` // standard JSON map of original bytes
Error error `json:"-"` // something go wrong? Error error `json:"-"` // something go wrong?
APISchema string `json:"-"` // API Schema for supplied spec type (2 or 3) APISchema string `json:"-"` // API Schema for supplied spec type (2 or 3)
Generated time.Time `json:"-"` Generated time.Time `json:"-"`
JsonParsingChannel chan bool `json:"-"` JsonParsingChannel chan bool `json:"-"`
OriginalIndentation int `json:"-"` // the original whitespace
} }
// GetJSONParsingChannel returns a channel that will close once async JSON parsing is completed. // 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 return specVersion, specVersion.Error
} }
// detect the original whitespace indentation
specVersion.OriginalIndentation = utils.DetermineWhitespaceLength(string(spec))
return specVersion, nil return specVersion, nil
} }

View File

@@ -167,10 +167,17 @@ func (d *document) RenderAndReload() ([]byte, Document, *DocumentModel[v3high.Do
// render the model as the correct type based on the source. // render the model as the correct type based on the source.
// https://github.com/pb33f/libopenapi/issues/105 // https://github.com/pb33f/libopenapi/issues/105
if d.info.SpecFileType == datamodel.JSONFileType { 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 { if d.info.SpecFileType == datamodel.YAMLFileType {
newBytes, renderError = d.highOpenAPI3Model.Model.Render() newBytes, renderError = d.highOpenAPI3Model.Model.RenderWithIndention(d.info.OriginalIndentation)
} }
if renderError != nil { if renderError != nil {

View File

@@ -622,3 +622,54 @@ func TestDocument_InputAsJSON(t *testing.T) {
assert.Equal(t, d, strings.TrimSpace(string(rend))) 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)))
}

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"regexp" "regexp"
"sort"
"strconv" "strconv"
"strings" "strings"
@@ -674,3 +675,19 @@ func CheckEnumForDuplicates(seq []*yaml.Node) []*yaml.Node {
} }
return res 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
}
}

View File

@@ -3,7 +3,7 @@ package utils
import ( import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"io/ioutil" "os"
"sync" "sync"
"testing" "testing"
) )
@@ -18,7 +18,7 @@ var (
func getPetstore() petstore { func getPetstore() petstore {
once.Do(func() { once.Do(func() {
psBytes, _ = ioutil.ReadFile("../test_specs/petstorev3.json") psBytes, _ = os.ReadFile("../test_specs/petstorev3.json")
}) })
return psBytes return psBytes
} }
@@ -785,7 +785,7 @@ func TestIsNodeRefValue_False(t *testing.T) {
func TestCheckEnumForDuplicates_Success(t *testing.T) { func TestCheckEnumForDuplicates_Success(t *testing.T) {
yml := "- yes\n- no\n- crisps" yml := "- yes\n- no\n- crisps"
var rootNode yaml.Node var rootNode yaml.Node
yaml.Unmarshal([]byte(yml), &rootNode) _ = yaml.Unmarshal([]byte(yml), &rootNode)
assert.Len(t, CheckEnumForDuplicates(rootNode.Content[0].Content), 0) 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) { func TestCheckEnumForDuplicates_Fail(t *testing.T) {
yml := "- yes\n- no\n- crisps\n- no" yml := "- yes\n- no\n- crisps\n- no"
var rootNode yaml.Node var rootNode yaml.Node
yaml.Unmarshal([]byte(yml), &rootNode) _ = yaml.Unmarshal([]byte(yml), &rootNode)
assert.Len(t, CheckEnumForDuplicates(rootNode.Content[0].Content), 1) 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" yml := "- yes\n- no\n- crisps\n- no\n- rice\n- yes\n- no"
var rootNode yaml.Node var rootNode yaml.Node
yaml.Unmarshal([]byte(yml), &rootNode) _ = yaml.Unmarshal([]byte(yml), &rootNode)
assert.Len(t, CheckEnumForDuplicates(rootNode.Content[0].Content), 3) 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, "$.components.schemas['OhNoWhy[HaveYouDoneThis]']", path)
assert.Equal(t, "OhNoWhy[HaveYouDoneThis]", segment) 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)))
}