Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/kcl/commands/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func appendLangFlags(o *options.RunOptions, flags *pflag.FlagSet) {
flags.StringVarP(&o.Branch, "branch", "b", "",
"Specify the branch for the Git artifact")
flags.StringVar(&o.Format, "format", "yaml",
"Specify the output format")
"Specify the output format (yaml, json, toml, xml)")
flags.BoolVarP(&o.DisableNone, "disable_none", "n", false,
"Disable dumping None values")
flags.BoolVarP(&o.Debug, "debug", "d", false,
Expand Down
3 changes: 3 additions & 0 deletions cmd/kcl/commands/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ For example, 'kcl run path/to/kcl.k' will run the file named path/to/kcl.k
# Run a single file and output TOML
kcl run path/to/kcl.k --format toml

# Run a single file and output XML
kcl run path/to/kcl.k --format xml

# Run multiple files
kcl run path/to/kcl1.k path/to/kcl2.k

Expand Down
44 changes: 44 additions & 0 deletions pkg/format/json/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright The KCL Authors. All rights reserved.

package json

import (
"bytes"
"encoding/json"

"kcl-lang.io/cli/pkg/format/yaml"
"kcl-lang.io/kcl-go/pkg/kcl"
)

// Single converts a single KCL result to JSON format.
func Single(result *kcl.KCLResultList) ([]byte, error) {
var out bytes.Buffer
err := json.Indent(&out, []byte(result.GetRawJsonResult()), "", " ")
if err != nil {
return nil, err
}
return []byte(out.String() + "\n"), nil
}

// Stream converts a YAML Stream to JSON format.
func Stream(yamlResult string) ([]byte, error) {
docs, err := yaml.ParseStream(yamlResult)
if err != nil {
return nil, err
}

var out bytes.Buffer
for i, doc := range docs {
jsonData, err := json.MarshalIndent(doc, "", " ")
if err != nil {
return nil, err
}
out.Write(jsonData)
if i < len(docs)-1 {
out.WriteString(",\n")
} else {
out.WriteString("\n")
}
}
return out.Bytes(), nil
}
194 changes: 194 additions & 0 deletions pkg/format/json/json_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
// Copyright The KCL Authors. All rights reserved.

package json

import (
"bytes"
"encoding/json"
"strings"
"testing"
)

func TestSingle(t *testing.T) {
tests := []struct {
name string
rawJSON string
wantErr bool
contains string
}{
{
name: "Simple object",
rawJSON: `{"name":"test","value":123}`,
wantErr: false,
contains: `"name": "test"`,
},
{
name: "Nested object",
rawJSON: `{"config":{"name":"test","value":123}}`,
wantErr: false,
contains: `"config": {`,
},
{
name: "Array",
rawJSON: `[{"name":"first"},{"name":"second"}]`,
wantErr: false,
contains: `"name": "first"`,
},
{
name: "Valid JSON with indentation",
rawJSON: `{"a":1,"b":2,"c":3}`,
wantErr: false,
contains: `"a": 1`,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create a mock result using the actual KCLResultList structure
// For testing purposes, we'll test the JSON formatting logic directly
var out bytes.Buffer
err := json.Indent(&out, []byte(tt.rawJSON), "", " ")
if (err != nil) != tt.wantErr {
t.Errorf("json.Indent() error = %v, wantErr %v", err, tt.wantErr)
return
}
result := out.String() + "\n"

if !tt.wantErr && !strings.Contains(result, tt.contains) {
t.Errorf("Result = %v, want to contain %v", result, tt.contains)
}
// Check that result is properly formatted (contains newlines and indentation)
if !tt.wantErr && !strings.Contains(result, "\n") {
t.Errorf("Result should be formatted with newlines")
}
})
}
}

func TestStream(t *testing.T) {
tests := []struct {
name string
yamlStream string
wantErr bool
docCount int
contains []string
}{
{
name: "YAML Stream with 2 documents",
yamlStream: "---\nname: First\nvalue: 1\n---\nname: Second\nvalue: 2\n",
wantErr: false,
docCount: 2,
contains: []string{`"name": "First"`, `"name": "Second"`},
},
{
name: "YAML Stream with 3 documents",
yamlStream: "---\na: 1\n---\nb: 2\n---\nc: 3\n",
wantErr: false,
docCount: 3,
contains: []string{`"a": 1`, `"b": 2`, `"c": 3`},
},
{
name: "YAML Stream with nested structures",
yamlStream: "---\nconfig:\n name: test\n value: 123\n---\nconfig:\n name: test2\n value: 456\n",
wantErr: false,
docCount: 2,
contains: []string{`"config": {`, `"name": "test"`, `"name": "test2"`},
},
{
name: "Single document (no stream)",
yamlStream: "name: test\nvalue: 123\n",
wantErr: false,
docCount: 1,
contains: []string{`"name": "test"`, `"value": 123`},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Stream(tt.yamlStream)
if (err != nil) != tt.wantErr {
t.Errorf("Stream() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
resultStr := string(result)
// Check that all expected strings are in the result
for _, expected := range tt.contains {
if !strings.Contains(resultStr, expected) {
t.Errorf("Stream() result = %v, want to contain %v", resultStr, expected)
}
}
// Note: JSON Stream output is not a single valid JSON, but multiple objects
// Each individual document is valid JSON, verified by checking format
if !strings.Contains(resultStr, "{") {
t.Errorf("Stream() result should contain JSON objects")
}
}
})
}
}

func TestStreamFormat(t *testing.T) {
yamlStream := "---\nname: First\nvalue: 1\n---\nname: Second\nvalue: 2\n"

result, err := Stream(yamlStream)
if err != nil {
t.Fatalf("Stream() error = %v", err)
}

resultStr := string(result)

// Check that documents are separated by commas
if !strings.Contains(resultStr, "},\n{") {
t.Errorf("Stream() result should contain comma separators between documents")
}

// Check that result ends with newline
if !strings.HasSuffix(resultStr, "\n") {
t.Errorf("Stream() result should end with newline")
}

// Verify it contains both documents
if !strings.Contains(resultStr, `"name": "First"`) {
t.Errorf("Result should contain first document")
}
if !strings.Contains(resultStr, `"name": "Second"`) {
t.Errorf("Result should contain second document")
}

// Note: The result is multiple JSON objects separated by commas
// Each individual document (between { and }) should be valid JSON
// Extract first document: from { to first }
firstDocStart := strings.Index(resultStr, "{")
firstDocEnd := strings.Index(resultStr, "}")
if firstDocStart == -1 || firstDocEnd == -1 {
t.Fatalf("Could not find document boundaries")
}
firstDoc := resultStr[firstDocStart : firstDocEnd+1]

var doc1 map[string]interface{}
if err := json.Unmarshal([]byte(firstDoc), &doc1); err != nil {
t.Errorf("First document is not valid JSON: %v\nContent: %s", err, firstDoc)
}

if doc1["name"] != "First" {
t.Errorf("First document name = %v, want 'First'", doc1["name"])
}

// Extract second document
secondDocStart := strings.LastIndex(resultStr, "{")
secondDocEnd := strings.LastIndex(resultStr, "}")
if secondDocStart == -1 || secondDocEnd == -1 {
t.Fatalf("Could not find second document boundaries")
}
secondDoc := resultStr[secondDocStart : secondDocEnd+1]

var doc2 map[string]interface{}
if err := json.Unmarshal([]byte(secondDoc), &doc2); err != nil {
t.Errorf("Second document is not valid JSON: %v\nContent: %s", err, secondDoc)
}

if doc2["name"] != "Second" {
t.Errorf("Second document name = %v, want 'Second'", doc2["name"])
}
}
76 changes: 76 additions & 0 deletions pkg/format/toml/toml.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright The KCL Authors. All rights reserved.

package toml

import (
"bytes"
"fmt"

"github.com/goccy/go-yaml"
yamlformat "kcl-lang.io/cli/pkg/format/yaml"
"kcl-lang.io/kcl-go/pkg/3rdparty/toml"
"kcl-lang.io/kcl-go/pkg/tools/gen"
)

// Single converts a single KCL result to TOML format.
func Single(yamlResult string, sortKeys bool) ([]byte, error) {
var out []byte
var err error
if sortKeys {
yamlData := make(map[string]any)
if err := yaml.UnmarshalWithOptions([]byte(yamlResult), &yamlData); err != nil {
return nil, err
}
out, err = toml.Marshal(&yamlData)
} else {
yamlData := &yaml.MapSlice{}
if err := yaml.UnmarshalWithOptions([]byte(yamlResult), yamlData, yaml.UseOrderedMap()); err != nil {
return nil, err
}
out, err = gen.MarshalTOML(yamlData)
}
if err != nil {
return nil, err
}
return []byte(string(out) + "\n"), nil
}

// Stream converts a YAML Stream to TOML format with document separators.
func Stream(yamlResult string, sortKeys bool) ([]byte, error) {
docs, err := yamlformat.ParseStream(yamlResult)
if err != nil {
return nil, err
}

var out bytes.Buffer
for i, doc := range docs {
var tomlData []byte
var err error
if sortKeys {
if data, ok := doc.(map[string]any); ok {
tomlData, err = toml.Marshal(&data)
} else {
return nil, fmt.Errorf("document %d is not a map", i+1)
}
} else {
// Convert to MapSlice for ordered output
yamlBytes, err := yaml.Marshal(doc)
if err != nil {
return nil, err
}
yamlData := &yaml.MapSlice{}
if err := yaml.UnmarshalWithOptions(yamlBytes, yamlData, yaml.UseOrderedMap()); err != nil {
return nil, err
}
tomlData, err = gen.MarshalTOML(yamlData)
}
if err != nil {
return nil, err
}
out.Write(tomlData)
if i < len(docs)-1 {
out.WriteString("\n# --- Document separator ---\n\n")
}
}
return out.Bytes(), nil
}
Loading