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