mirror of
https://github.com/LukeHagar/libopenapi.git
synced 2025-12-06 12:37:49 +00:00
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:
@@ -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:
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
11
document.go
11
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 {
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user