Skip to content

Commit

Permalink
Original indention and text delimiter style retained when rendering p…
Browse files Browse the repository at this point in the history
…b33f#106

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

Signed-off-by: Dave Shanley <[email protected]>

# fixes 106
  • Loading branch information
daveshanley committed Jun 17, 2023
1 parent 5b128c0 commit e1f0f69
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 52 deletions.
33 changes: 25 additions & 8 deletions datamodel/high/node_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type NodeEntry struct {
Value any
StringValue string
Line int
Style yaml.Style
RenderZero bool
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
18 changes: 16 additions & 2 deletions datamodel/high/v3/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) {
Expand Down
46 changes: 23 additions & 23 deletions datamodel/high/v3/media_type_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
28 changes: 16 additions & 12 deletions datamodel/spec_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}

Expand Down
11 changes: 9 additions & 2 deletions document.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
51 changes: 51 additions & 0 deletions document_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
}
17 changes: 17 additions & 0 deletions utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"net/url"
"regexp"
"sort"
"strconv"
"strings"

Expand Down Expand Up @@ -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
}
}
Loading

0 comments on commit e1f0f69

Please sign in to comment.