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
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:

View File

@@ -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) {

View File

@@ -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()

View File

@@ -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
}

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.
// 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 {

View File

@@ -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)))
}

View File

@@ -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
}
}

View File

@@ -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)))
}