Skip to content

Commit e57f95e

Browse files
authored
Merge pull request #295 from kcl-lang/feat-xml-format
feat: xml format output
2 parents b378603 + c2e53d1 commit e57f95e

File tree

13 files changed

+1215
-32
lines changed

13 files changed

+1215
-32
lines changed

cmd/kcl/commands/flags.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func appendLangFlags(o *options.RunOptions, flags *pflag.FlagSet) {
2121
flags.StringVarP(&o.Branch, "branch", "b", "",
2222
"Specify the branch for the Git artifact")
2323
flags.StringVar(&o.Format, "format", "yaml",
24-
"Specify the output format")
24+
"Specify the output format (yaml, json, toml, xml)")
2525
flags.BoolVarP(&o.DisableNone, "disable_none", "n", false,
2626
"Disable dumping None values")
2727
flags.BoolVarP(&o.Debug, "debug", "d", false,

cmd/kcl/commands/run.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ For example, 'kcl run path/to/kcl.k' will run the file named path/to/kcl.k
2424
# Run a single file and output TOML
2525
kcl run path/to/kcl.k --format toml
2626
27+
# Run a single file and output XML
28+
kcl run path/to/kcl.k --format xml
29+
2730
# Run multiple files
2831
kcl run path/to/kcl1.k path/to/kcl2.k
2932

pkg/format/json/json.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright The KCL Authors. All rights reserved.
2+
3+
package json
4+
5+
import (
6+
"bytes"
7+
"encoding/json"
8+
9+
"kcl-lang.io/cli/pkg/format/yaml"
10+
"kcl-lang.io/kcl-go/pkg/kcl"
11+
)
12+
13+
// Single converts a single KCL result to JSON format.
14+
func Single(result *kcl.KCLResultList) ([]byte, error) {
15+
var out bytes.Buffer
16+
err := json.Indent(&out, []byte(result.GetRawJsonResult()), "", " ")
17+
if err != nil {
18+
return nil, err
19+
}
20+
return []byte(out.String() + "\n"), nil
21+
}
22+
23+
// Stream converts a YAML Stream to JSON format.
24+
func Stream(yamlResult string) ([]byte, error) {
25+
docs, err := yaml.ParseStream(yamlResult)
26+
if err != nil {
27+
return nil, err
28+
}
29+
30+
var out bytes.Buffer
31+
for i, doc := range docs {
32+
jsonData, err := json.MarshalIndent(doc, "", " ")
33+
if err != nil {
34+
return nil, err
35+
}
36+
out.Write(jsonData)
37+
if i < len(docs)-1 {
38+
out.WriteString(",\n")
39+
} else {
40+
out.WriteString("\n")
41+
}
42+
}
43+
return out.Bytes(), nil
44+
}

pkg/format/json/json_test.go

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// Copyright The KCL Authors. All rights reserved.
2+
3+
package json
4+
5+
import (
6+
"bytes"
7+
"encoding/json"
8+
"strings"
9+
"testing"
10+
)
11+
12+
func TestSingle(t *testing.T) {
13+
tests := []struct {
14+
name string
15+
rawJSON string
16+
wantErr bool
17+
contains string
18+
}{
19+
{
20+
name: "Simple object",
21+
rawJSON: `{"name":"test","value":123}`,
22+
wantErr: false,
23+
contains: `"name": "test"`,
24+
},
25+
{
26+
name: "Nested object",
27+
rawJSON: `{"config":{"name":"test","value":123}}`,
28+
wantErr: false,
29+
contains: `"config": {`,
30+
},
31+
{
32+
name: "Array",
33+
rawJSON: `[{"name":"first"},{"name":"second"}]`,
34+
wantErr: false,
35+
contains: `"name": "first"`,
36+
},
37+
{
38+
name: "Valid JSON with indentation",
39+
rawJSON: `{"a":1,"b":2,"c":3}`,
40+
wantErr: false,
41+
contains: `"a": 1`,
42+
},
43+
}
44+
45+
for _, tt := range tests {
46+
t.Run(tt.name, func(t *testing.T) {
47+
// Create a mock result using the actual KCLResultList structure
48+
// For testing purposes, we'll test the JSON formatting logic directly
49+
var out bytes.Buffer
50+
err := json.Indent(&out, []byte(tt.rawJSON), "", " ")
51+
if (err != nil) != tt.wantErr {
52+
t.Errorf("json.Indent() error = %v, wantErr %v", err, tt.wantErr)
53+
return
54+
}
55+
result := out.String() + "\n"
56+
57+
if !tt.wantErr && !strings.Contains(result, tt.contains) {
58+
t.Errorf("Result = %v, want to contain %v", result, tt.contains)
59+
}
60+
// Check that result is properly formatted (contains newlines and indentation)
61+
if !tt.wantErr && !strings.Contains(result, "\n") {
62+
t.Errorf("Result should be formatted with newlines")
63+
}
64+
})
65+
}
66+
}
67+
68+
func TestStream(t *testing.T) {
69+
tests := []struct {
70+
name string
71+
yamlStream string
72+
wantErr bool
73+
docCount int
74+
contains []string
75+
}{
76+
{
77+
name: "YAML Stream with 2 documents",
78+
yamlStream: "---\nname: First\nvalue: 1\n---\nname: Second\nvalue: 2\n",
79+
wantErr: false,
80+
docCount: 2,
81+
contains: []string{`"name": "First"`, `"name": "Second"`},
82+
},
83+
{
84+
name: "YAML Stream with 3 documents",
85+
yamlStream: "---\na: 1\n---\nb: 2\n---\nc: 3\n",
86+
wantErr: false,
87+
docCount: 3,
88+
contains: []string{`"a": 1`, `"b": 2`, `"c": 3`},
89+
},
90+
{
91+
name: "YAML Stream with nested structures",
92+
yamlStream: "---\nconfig:\n name: test\n value: 123\n---\nconfig:\n name: test2\n value: 456\n",
93+
wantErr: false,
94+
docCount: 2,
95+
contains: []string{`"config": {`, `"name": "test"`, `"name": "test2"`},
96+
},
97+
{
98+
name: "Single document (no stream)",
99+
yamlStream: "name: test\nvalue: 123\n",
100+
wantErr: false,
101+
docCount: 1,
102+
contains: []string{`"name": "test"`, `"value": 123`},
103+
},
104+
}
105+
106+
for _, tt := range tests {
107+
t.Run(tt.name, func(t *testing.T) {
108+
result, err := Stream(tt.yamlStream)
109+
if (err != nil) != tt.wantErr {
110+
t.Errorf("Stream() error = %v, wantErr %v", err, tt.wantErr)
111+
return
112+
}
113+
if !tt.wantErr {
114+
resultStr := string(result)
115+
// Check that all expected strings are in the result
116+
for _, expected := range tt.contains {
117+
if !strings.Contains(resultStr, expected) {
118+
t.Errorf("Stream() result = %v, want to contain %v", resultStr, expected)
119+
}
120+
}
121+
// Note: JSON Stream output is not a single valid JSON, but multiple objects
122+
// Each individual document is valid JSON, verified by checking format
123+
if !strings.Contains(resultStr, "{") {
124+
t.Errorf("Stream() result should contain JSON objects")
125+
}
126+
}
127+
})
128+
}
129+
}
130+
131+
func TestStreamFormat(t *testing.T) {
132+
yamlStream := "---\nname: First\nvalue: 1\n---\nname: Second\nvalue: 2\n"
133+
134+
result, err := Stream(yamlStream)
135+
if err != nil {
136+
t.Fatalf("Stream() error = %v", err)
137+
}
138+
139+
resultStr := string(result)
140+
141+
// Check that documents are separated by commas
142+
if !strings.Contains(resultStr, "},\n{") {
143+
t.Errorf("Stream() result should contain comma separators between documents")
144+
}
145+
146+
// Check that result ends with newline
147+
if !strings.HasSuffix(resultStr, "\n") {
148+
t.Errorf("Stream() result should end with newline")
149+
}
150+
151+
// Verify it contains both documents
152+
if !strings.Contains(resultStr, `"name": "First"`) {
153+
t.Errorf("Result should contain first document")
154+
}
155+
if !strings.Contains(resultStr, `"name": "Second"`) {
156+
t.Errorf("Result should contain second document")
157+
}
158+
159+
// Note: The result is multiple JSON objects separated by commas
160+
// Each individual document (between { and }) should be valid JSON
161+
// Extract first document: from { to first }
162+
firstDocStart := strings.Index(resultStr, "{")
163+
firstDocEnd := strings.Index(resultStr, "}")
164+
if firstDocStart == -1 || firstDocEnd == -1 {
165+
t.Fatalf("Could not find document boundaries")
166+
}
167+
firstDoc := resultStr[firstDocStart : firstDocEnd+1]
168+
169+
var doc1 map[string]interface{}
170+
if err := json.Unmarshal([]byte(firstDoc), &doc1); err != nil {
171+
t.Errorf("First document is not valid JSON: %v\nContent: %s", err, firstDoc)
172+
}
173+
174+
if doc1["name"] != "First" {
175+
t.Errorf("First document name = %v, want 'First'", doc1["name"])
176+
}
177+
178+
// Extract second document
179+
secondDocStart := strings.LastIndex(resultStr, "{")
180+
secondDocEnd := strings.LastIndex(resultStr, "}")
181+
if secondDocStart == -1 || secondDocEnd == -1 {
182+
t.Fatalf("Could not find second document boundaries")
183+
}
184+
secondDoc := resultStr[secondDocStart : secondDocEnd+1]
185+
186+
var doc2 map[string]interface{}
187+
if err := json.Unmarshal([]byte(secondDoc), &doc2); err != nil {
188+
t.Errorf("Second document is not valid JSON: %v\nContent: %s", err, secondDoc)
189+
}
190+
191+
if doc2["name"] != "Second" {
192+
t.Errorf("Second document name = %v, want 'Second'", doc2["name"])
193+
}
194+
}

pkg/format/toml/toml.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright The KCL Authors. All rights reserved.
2+
3+
package toml
4+
5+
import (
6+
"bytes"
7+
"fmt"
8+
9+
"github.com/goccy/go-yaml"
10+
yamlformat "kcl-lang.io/cli/pkg/format/yaml"
11+
"kcl-lang.io/kcl-go/pkg/3rdparty/toml"
12+
"kcl-lang.io/kcl-go/pkg/tools/gen"
13+
)
14+
15+
// Single converts a single KCL result to TOML format.
16+
func Single(yamlResult string, sortKeys bool) ([]byte, error) {
17+
var out []byte
18+
var err error
19+
if sortKeys {
20+
yamlData := make(map[string]any)
21+
if err := yaml.UnmarshalWithOptions([]byte(yamlResult), &yamlData); err != nil {
22+
return nil, err
23+
}
24+
out, err = toml.Marshal(&yamlData)
25+
} else {
26+
yamlData := &yaml.MapSlice{}
27+
if err := yaml.UnmarshalWithOptions([]byte(yamlResult), yamlData, yaml.UseOrderedMap()); err != nil {
28+
return nil, err
29+
}
30+
out, err = gen.MarshalTOML(yamlData)
31+
}
32+
if err != nil {
33+
return nil, err
34+
}
35+
return []byte(string(out) + "\n"), nil
36+
}
37+
38+
// Stream converts a YAML Stream to TOML format with document separators.
39+
func Stream(yamlResult string, sortKeys bool) ([]byte, error) {
40+
docs, err := yamlformat.ParseStream(yamlResult)
41+
if err != nil {
42+
return nil, err
43+
}
44+
45+
var out bytes.Buffer
46+
for i, doc := range docs {
47+
var tomlData []byte
48+
var err error
49+
if sortKeys {
50+
if data, ok := doc.(map[string]any); ok {
51+
tomlData, err = toml.Marshal(&data)
52+
} else {
53+
return nil, fmt.Errorf("document %d is not a map", i+1)
54+
}
55+
} else {
56+
// Convert to MapSlice for ordered output
57+
yamlBytes, err := yaml.Marshal(doc)
58+
if err != nil {
59+
return nil, err
60+
}
61+
yamlData := &yaml.MapSlice{}
62+
if err := yaml.UnmarshalWithOptions(yamlBytes, yamlData, yaml.UseOrderedMap()); err != nil {
63+
return nil, err
64+
}
65+
tomlData, err = gen.MarshalTOML(yamlData)
66+
}
67+
if err != nil {
68+
return nil, err
69+
}
70+
out.Write(tomlData)
71+
if i < len(docs)-1 {
72+
out.WriteString("\n# --- Document separator ---\n\n")
73+
}
74+
}
75+
return out.Bytes(), nil
76+
}

0 commit comments

Comments
 (0)