-
Notifications
You must be signed in to change notification settings - Fork 1
/
schema.go
132 lines (122 loc) · 3.93 KB
/
schema.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
package pkg
import (
"bytes"
"encoding/json"
"fmt"
"cuelang.org/go/cue"
"cuelang.org/go/cue/format"
"cuelang.org/go/encoding/openapi"
"cuelang.org/go/encoding/yaml"
"github.com/getkin/kin-openapi/openapi3"
"github.com/pkg/errors"
)
// GenerateSchemaFromValues generate OpenAPIv3 schema based on Chart Values
// file.
func GenerateSchemaFromValues(values []byte) ([]byte, error) {
r := cue.Runtime{}
// convert Values yaml to CUE
ins, err := yaml.Decode(&r, "", string(values))
if err != nil {
return nil, errors.Wrap(err, "cannot decode Values.yaml to CUE")
}
// get the streamed CUE including the comments which will be used as
// 'description' in the schema
c, err := format.Node(ins.Value().Syntax(cue.Docs(true)), format.Simplify())
if err != nil {
return nil, errors.Wrap(err, "cannot format CUE generated from Values.yaml")
}
valuesIdentifier := "values"
// cue openapi encoder only works on top-level identifier, we have to add
// an identifier manually
valuesStr := fmt.Sprintf("#%s:{\n%s\n}", valuesIdentifier, string(c))
r = cue.Runtime{}
ins, err = r.Compile("-", valuesStr)
if err != nil {
return nil, errors.Wrap(err, "cannot compile CUE generated from Values.yaml")
}
if ins.Err != nil {
return nil, errors.Wrap(ins.Err, "cannot compile CUE generated from Values.yaml")
}
// generate OpenAPIv3 schema through cue openapi encoder
rawSchema, err := openapi.Gen(ins, &openapi.Config{})
if err != nil {
return nil, errors.Wrap(ins.Err, "cannot generate OpenAPIv3 schema")
}
rawSchema, err = makeSwaggerCompatible(rawSchema)
if err != nil {
return nil, errors.WithMessage(err, "cannot make CUE-generated schema compatible with Swagger")
}
var out = &bytes.Buffer{}
_ = json.Indent(out, rawSchema, "", " ")
// load schema into Swagger to validate it compatible with Swagger OpenAPIv3
fullSchemaBySwagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromData(out.Bytes())
if err != nil {
return nil, errors.Wrap(err, "cannot load schema by SwaggerLoader")
}
valuesSchema := fullSchemaBySwagger.Components.Schemas[valuesIdentifier].Value
changeEnumToDefault(valuesSchema)
b, err := valuesSchema.MarshalJSON()
if err != nil {
return nil, errors.Wrap(err, "cannot marshall Values schema")
}
_ = json.Indent(out, b, "", " ")
return out.Bytes(), nil
}
// cue openapi encoder converts default in Chart Values as enum in schema
// changing enum to default makes the schema consistent with Chart Values
func changeEnumToDefault(schema *openapi3.Schema) {
t := schema.Type
switch t {
case "object":
for _, v := range schema.Properties {
s := v.Value
changeEnumToDefault(s)
}
case "array":
if schema.Items != nil {
changeEnumToDefault(schema.Items.Value)
}
}
// change enum to default
if len(schema.Enum) > 0 {
schema.Default = schema.Enum[0]
schema.Enum = nil
}
// remove all required fields, because fields in Values.yml are all optional
schema.Required = nil
}
// cue openapi encoder converts 'items' field in an array type field into array,
// that's not compatible with OpenAPIv3. 'items' field should be an object.
func makeSwaggerCompatible(d []byte) ([]byte, error) {
m := map[string]interface{}{}
err := json.Unmarshal(d, &m)
if err != nil {
return nil, errors.Wrap(err, "cannot unmarshall schema")
}
handleItemsOfArrayType(m)
b, err := json.Marshal(m)
if err != nil {
return nil, errors.Wrap(err, "cannot marshall schema")
}
return b, nil
}
// handleItemsOfArrayType will convert all 'items' of array type from array to object
// and remove enum in the items
func handleItemsOfArrayType(t map[string]interface{}) {
for _, v := range t {
if next, ok := v.(map[string]interface{}); ok {
handleItemsOfArrayType(next)
}
}
if t["type"] == "array" {
if i, ok := t["items"].([]interface{}); ok {
if len(i) > 0 {
if itemSpec, ok := i[0].(map[string]interface{}); ok {
handleItemsOfArrayType(itemSpec)
itemSpec["enum"] = nil
t["items"] = itemSpec
}
}
}
}
}