From 2586ce5e9f733a58d1317ad6adb1b60519d5feca Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Mon, 6 Apr 2026 01:41:14 +0530 Subject: [PATCH 1/2] fix(generator): quote multiline default strings correctly Signed-off-by: Asish Kumar --- pkg/swagger/generator/language.go | 28 ++++++++++++++++----- pkg/swagger/generator/template_repo_test.go | 19 ++++++++++++++ 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/pkg/swagger/generator/language.go b/pkg/swagger/generator/language.go index a06d10a..7684b39 100644 --- a/pkg/swagger/generator/language.go +++ b/pkg/swagger/generator/language.go @@ -23,6 +23,7 @@ import ( "reflect" "regexp" "sort" + "strconv" "strings" "gopkg.in/yaml.v2" @@ -158,6 +159,14 @@ func (l *LanguageOpts) ManglePackagePath(name string, suffix string) string { } func (l *LanguageOpts) ToKclValue(data interface{}) string { + return l.toKclValue(data, true) +} + +func (l *LanguageOpts) ToKclDocValue(data interface{}) string { + return l.toKclValue(data, false) +} + +func (l *LanguageOpts) toKclValue(data interface{}, allowMultiline bool) string { if data == nil { return "None" } @@ -167,7 +176,7 @@ func (l *LanguageOpts) ToKclValue(data interface{}) string { var mapContents []string iter := value.MapRange() for iter.Next() { - mapContents = append(mapContents, fmt.Sprintf("%s: %s", l.ToKclValue(iter.Key()), l.ToKclValue(iter.Value()))) + mapContents = append(mapContents, fmt.Sprintf("%s: %s", l.toKclValue(iter.Key(), allowMultiline), l.toKclValue(iter.Value(), allowMultiline))) } content := strings.Join(mapContents, ", ") return fmt.Sprintf("{%s}", content) @@ -178,7 +187,7 @@ func (l *LanguageOpts) ToKclValue(data interface{}) string { for _, v := range dataSlice { k := v.Key v := v.Value - dictContents = append(dictContents, fmt.Sprintf("%s: %s", l.ToKclValue(k), l.ToKclValue(v))) + dictContents = append(dictContents, fmt.Sprintf("%s: %s", l.toKclValue(k, allowMultiline), l.toKclValue(v, allowMultiline))) } content := strings.Join(dictContents, ", ") return fmt.Sprintf("{%s}", content) @@ -186,12 +195,12 @@ func (l *LanguageOpts) ToKclValue(data interface{}) string { // if is a normal slice var sliceContents []string for i := 0; i < value.Len(); i++ { - sliceContents = append(sliceContents, l.ToKclValue(value.Index(i).Interface())) + sliceContents = append(sliceContents, l.toKclValue(value.Index(i).Interface(), allowMultiline)) } content := strings.Join(sliceContents, ", ") return fmt.Sprintf("[%s]", content) case reflect.String: - return fmt.Sprintf("\"%s\"", data) + return quoteKclString(data.(string), allowMultiline) case reflect.Int, reflect.Int8, reflect.Int16, @@ -213,14 +222,14 @@ func (l *LanguageOpts) ToKclValue(data interface{}) string { default: // Reflect value if dataValue, ok := data.(reflect.Value); ok { - return l.ToKclValue(dataValue.Interface()) + return l.toKclValue(dataValue.Interface(), allowMultiline) } else if dataSlice, ok := data.(yaml.MapSlice); ok { // If is a MapSlice var dictContents []string for _, v := range dataSlice { k := v.Key v := v.Value - dictContents = append(dictContents, fmt.Sprintf("%s: %s", l.ToKclValue(k), l.ToKclValue(v))) + dictContents = append(dictContents, fmt.Sprintf("%s: %s", l.toKclValue(k, allowMultiline), l.toKclValue(v, allowMultiline))) } content := strings.Join(dictContents, ", ") return fmt.Sprintf("{%s}", content) @@ -236,6 +245,13 @@ func (l *LanguageOpts) ToKclValue(data interface{}) string { } } +func quoteKclString(data string, allowMultiline bool) string { + if allowMultiline && strings.ContainsAny(data, "\r\n") && !strings.Contains(data, `"""`) { + return fmt.Sprintf("\"\"\"%s\"\"\"", data) + } + return strconv.Quote(data) +} + // FormatContent formats a file with a language specific formatter func (l *LanguageOpts) FormatContent(name string, content []byte) ([]byte, error) { if l.formatFunc != nil { diff --git a/pkg/swagger/generator/template_repo_test.go b/pkg/swagger/generator/template_repo_test.go index a146bd3..014d83e 100644 --- a/pkg/swagger/generator/template_repo_test.go +++ b/pkg/swagger/generator/template_repo_test.go @@ -33,6 +33,16 @@ func TestToKCLValue(t *testing.T) { value: "hello", expect: "\"hello\"", }, + { + name: "string-with-quote", + value: "hello \"world\"", + expect: "\"hello \\\"world\\\"\"", + }, + { + name: "multiline-string", + value: "#!/bin/bash\nset -e\necho \"line one\"\n", + expect: "\"\"\"#!/bin/bash\nset -e\necho \"line one\"\n\"\"\"", + }, { name: "map-string-int", value: yaml.MapSlice{ @@ -100,6 +110,15 @@ func TestToKCLValue(t *testing.T) { } } +func TestToKCLDocValue(t *testing.T) { + opts := LanguageOpts{} + got := opts.ToKclDocValue("#!/bin/bash\nset -e\necho \"line one\"\n") + expect := "\"#!/bin/bash\\nset -e\\necho \\\"line one\\\"\\n\"" + if got != expect { + t.Fatalf("unexpected output, expect:\n%s\ngot:\n%s\n", expect, got) + } +} + func TestPadDocument(t *testing.T) { cases := []struct { doc string From 2b459d8045b425725510ba0e44e82205654546ca Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Mon, 6 Apr 2026 01:42:37 +0530 Subject: [PATCH 2/2] fix(generator): escape multiline defaults in schema docstrings Signed-off-by: Asish Kumar --- pkg/swagger/generator/support_test.go | 67 +++++++++++++++++++ pkg/swagger/generator/template_repo.go | 1 + .../generator/templates/docstring.gotmpl | 2 +- .../generator/templates/propertydoc.gotmpl | 2 +- 4 files changed, 70 insertions(+), 2 deletions(-) diff --git a/pkg/swagger/generator/support_test.go b/pkg/swagger/generator/support_test.go index 6fa468d..bb5d679 100644 --- a/pkg/swagger/generator/support_test.go +++ b/pkg/swagger/generator/support_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "testing" crdGen "kcl-lang.io/kcl-openapi/pkg/kube_resource/generator" @@ -34,6 +35,72 @@ func TestGenerate_CRD2KCL(t *testing.T) { utils.DoTestDirs(t, utils.KubeTestDirs, apiConvertModel, true) } +func TestGenerate_CRD2KCL_MultilineStringDefault(t *testing.T) { + tempDir := t.TempDir() + specPath := filepath.Join(tempDir, "crd.yaml") + if err := os.WriteFile(specPath, []byte(`apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: examples.example.com +spec: + group: example.com + names: + kind: Example + plural: examples + singular: example + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + singleLine: + type: string + default: "hello world" + multiLine: + type: string + default: | + #!/bin/bash + set -e + echo "line one" + echo "line two" +`), 0o644); err != nil { + t.Fatalf("write CRD spec failed: %v", err) + } + + if err := apiConvertModel(utils.IntegrationGenOpts{ + SpecPath: specPath, + TargetDir: tempDir, + IsCrd: true, + ModelPackage: "models", + }); err != nil { + t.Fatalf("generate failed: %v", err) + } + + generatedPath := filepath.Join(tempDir, "models", "example_com_v1alpha1_example.k") + generated, err := os.ReadFile(generatedPath) + if err != nil { + t.Fatalf("read generated model failed: %v", err) + } + content := string(generated) + if !strings.Contains(content, `multiLine?: str = """#!/bin/bash +set -e +echo "line one" +echo "line two" +"""`) { + t.Fatalf("missing multiline default in generated model:\n%s", content) + } + if !strings.Contains(content, `multiLine : str, default is "#!/bin/bash\nset -e\necho \"line one\"\necho \"line two\"\n", optional`) { + t.Fatalf("missing escaped multiline doc default in generated model:\n%s", content) + } +} + func apiConvertModel(integrationGenOpts utils.IntegrationGenOpts) error { opts := new(GenOpts) opts.Spec = integrationGenOpts.SpecPath diff --git a/pkg/swagger/generator/template_repo.go b/pkg/swagger/generator/template_repo.go index 417b7bc..b71a5e8 100644 --- a/pkg/swagger/generator/template_repo.go +++ b/pkg/swagger/generator/template_repo.go @@ -140,6 +140,7 @@ func DefaultFuncMap(lang *LanguageOpts) template.FuncMap { return properties }, "toKCLValue": lang.ToKclValue, + "toKCLDocValue": lang.ToKclDocValue, "nonEmptyValue": lang.NonEmptyValue, "dict": func(values ...interface{}) (map[string]interface{}, error) { if len(values)%2 != 0 { diff --git a/pkg/swagger/generator/templates/docstring.gotmpl b/pkg/swagger/generator/templates/docstring.gotmpl index 82d626c..2ad4292 100644 --- a/pkg/swagger/generator/templates/docstring.gotmpl +++ b/pkg/swagger/generator/templates/docstring.gotmpl @@ -20,6 +20,6 @@ Examples -------- - demo = {{ toKCLValue .Example }} + demo = {{ toKCLDocValue .Example }} {{- end -}} {{- end }} diff --git a/pkg/swagger/generator/templates/propertydoc.gotmpl b/pkg/swagger/generator/templates/propertydoc.gotmpl index 99244a6..25ce882 100644 --- a/pkg/swagger/generator/templates/propertydoc.gotmpl +++ b/pkg/swagger/generator/templates/propertydoc.gotmpl @@ -1,4 +1,4 @@ {{ define "propertydoc" }} - {{ .EscapedName }} : {{ .KclType }}, default is {{ if .Default }}{{ toKCLValue .Default }}{{ else }}Undefined{{ end }}, {{ if not .Required }}optional{{else}}required{{ end }} + {{ .EscapedName }} : {{ .KclType }}, default is {{ if .Default }}{{ toKCLDocValue .Default }}{{ else }}Undefined{{ end }}, {{ if not .Required }}optional{{else}}required{{ end }} {{ template "introduction" . }} {{- end }}