Files
moby/api/scripts/swagger-spec/format.go
T
Paweł Gronowski 9e5299c63e api: Generate Swagger compatibility output from OpenAPI
Add a Go converter in a separate tooling module and reintroduce
swagger.yaml as generated Swagger 2.0 output. Preserve schema property
order, examples, and generator extensions, and reject unsupported
constructs. Use the union of response media types for Swagger's
operation-wide produces lists.

Leave historical versioned specifications and generated Go models
unchanged.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-10-29 11:34:26 +01:00

120 lines
3.5 KiB
Go

package main
import (
"bytes"
"encoding/json"
"errors"
"strings"
"go.yaml.in/yaml/v3"
)
// preserveJSONLayout restores multiline JSON objects and arrays from the source.
// yaml.Node retains flow style, but the YAML encoder discards its line breaks.
func preserveJSONLayout(source, output []byte, projected *yaml.Node) ([]byte, error) {
var emitted yaml.Node
if err := yaml.Unmarshal(output, &emitted); err != nil {
return nil, err
}
var result bytes.Buffer
position := 0
var walk func(*yaml.Node, *yaml.Node) error
walk = func(original, rendered *yaml.Node) error {
if original.Kind != rendered.Kind || len(original.Content) != len(rendered.Content) {
return errors.New("YAML encoder changed the projected structure")
}
src, sourceOK := jsonFragment(source, original)
dst, outputOK := jsonFragment(output, rendered)
if sourceOK && outputOK && bytes.Contains(src.text, []byte("\n")) {
// Source locations survive projection, so check the value as well:
// formatting must never restore data that projection changed.
var value yaml.Node
if err := yaml.Unmarshal(src.text, &value); err == nil && sameContent(value.Content[0], original) {
result.Write(output[position:dst.start])
lines := bytes.Split(src.text, []byte("\n"))
result.Write(lines[0])
sourceIndent := bytes.Repeat([]byte(" "), src.indent)
outputIndent := strings.Repeat(" ", dst.indent)
if original.Column == src.indent+1 {
// The encoder moved the opening delimiter beside the key.
// Keep continuation lines indented beyond that key.
outputIndent += " "
}
for _, line := range lines[1:] {
result.WriteByte('\n')
if len(bytes.TrimSpace(line)) != 0 {
result.WriteString(outputIndent)
result.Write(bytes.TrimPrefix(line, sourceIndent))
}
}
position = dst.start + len(dst.text)
return nil
}
}
for i, child := range original.Content {
if err := walk(child, rendered.Content[i]); err != nil {
return err
}
}
return nil
}
if err := walk(projected, emitted.Content[0]); err != nil {
return nil, err
}
result.Write(output[position:])
return result.Bytes(), nil
}
// sameContent includes mapping order, which matters to generated Go models.
func sameContent(a, b *yaml.Node) bool {
if a.Kind != b.Kind || a.Tag != b.Tag || a.Value != b.Value || len(a.Content) != len(b.Content) {
return false
}
for i, child := range a.Content {
if !sameContent(child, b.Content[i]) {
return false
}
}
return true
}
type jsonSource struct {
text []byte
start int
indent int
}
func jsonFragment(data []byte, node *yaml.Node) (jsonSource, bool) {
if node.Style&yaml.FlowStyle == 0 || node.Line < 1 || node.Column < 1 {
return jsonSource{}, false
}
lineStart := 0
for line := 1; line < node.Line; line++ {
end := bytes.IndexByte(data[lineStart:], '\n')
if end < 0 {
return jsonSource{}, false
}
lineStart += end + 1
}
line, _, _ := bytes.Cut(data[lineStart:], []byte("\n"))
// YAML columns count characters, not bytes.
runes := []rune(string(line))
if node.Column > len(runes) {
return jsonSource{}, false
}
start := lineStart + len(string(runes[:node.Column-1]))
if data[start] != '{' && data[start] != '[' {
return jsonSource{}, false
}
var raw json.RawMessage
decoder := json.NewDecoder(bytes.NewReader(data[start:]))
if err := decoder.Decode(&raw); err != nil {
return jsonSource{}, false
}
return jsonSource{
text: data[start : start+int(decoder.InputOffset())],
start: start,
indent: len(line) - len(bytes.TrimLeft(line, " ")),
}, true
}