Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 1 | // Copyright 2019 CUE Authors |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | |
| 15 | package jsonschema |
| 16 | |
| 17 | // TODO: |
| 18 | // - replace converter from YAML to CUE to CUE (schema) to CUE. |
| 19 | // - define OpenAPI definitions als CUE. |
| 20 | |
| 21 | import ( |
| 22 | "fmt" |
Marcel van Lohuizen | 78e21c1 | 2020-04-14 15:24:34 +0200 | [diff] [blame] | 23 | "math/bits" |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 24 | "sort" |
| 25 | "strings" |
| 26 | |
| 27 | "cuelang.org/go/cue" |
| 28 | "cuelang.org/go/cue/ast" |
| 29 | "cuelang.org/go/cue/errors" |
| 30 | "cuelang.org/go/cue/token" |
| 31 | "cuelang.org/go/internal" |
| 32 | ) |
| 33 | |
Marcel van Lohuizen | 435989a | 2020-05-06 18:43:58 +0200 | [diff] [blame] | 34 | // rootDefs defines the top-level name of the map of definitions that do not |
| 35 | // have a valid identifier name. |
| 36 | // |
| 37 | // TODO: find something more principled, like allowing #."a-b" or `#a-b`. |
| 38 | const rootDefs = "#def" |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 39 | |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 40 | // A decoder converts JSON schema to CUE. |
| 41 | type decoder struct { |
Marcel van Lohuizen | 47d9870 | 2020-01-17 21:51:39 +0100 | [diff] [blame] | 42 | cfg *Config |
| 43 | |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 44 | errs errors.Error |
| 45 | imports map[string]*ast.Ident |
| 46 | |
| 47 | definitions []ast.Decl |
| 48 | } |
| 49 | |
| 50 | // addImport registers |
| 51 | func (d *decoder) addImport(pkg string) *ast.Ident { |
| 52 | ident, ok := d.imports[pkg] |
| 53 | if !ok { |
| 54 | ident = ast.NewIdent(pkg) |
| 55 | d.imports[pkg] = ident |
| 56 | } |
| 57 | return ident |
| 58 | } |
| 59 | |
Marcel van Lohuizen | 671b956 | 2020-03-10 12:42:45 +0100 | [diff] [blame] | 60 | func (d *decoder) decode(v cue.Value) *ast.File { |
| 61 | f := &ast.File{} |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 62 | |
Marcel van Lohuizen | bd98b13 | 2020-03-05 15:38:21 +0100 | [diff] [blame] | 63 | if pkgName := d.cfg.PkgName; pkgName != "" { |
| 64 | pkg := &ast.Package{Name: ast.NewIdent(pkgName)} |
Marcel van Lohuizen | 671b956 | 2020-03-10 12:42:45 +0100 | [diff] [blame] | 65 | f.Decls = append(f.Decls, pkg) |
| 66 | } |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 67 | |
Marcel van Lohuizen | 671b956 | 2020-03-10 12:42:45 +0100 | [diff] [blame] | 68 | var a []ast.Decl |
| 69 | |
| 70 | if d.cfg.Root == "" { |
Marcel van Lohuizen | 435989a | 2020-05-06 18:43:58 +0200 | [diff] [blame] | 71 | a = append(a, d.schema(nil, v)...) |
Marcel van Lohuizen | 671b956 | 2020-03-10 12:42:45 +0100 | [diff] [blame] | 72 | } else { |
| 73 | ref := d.parseRef(token.NoPos, d.cfg.Root) |
| 74 | if ref == nil { |
| 75 | return f |
| 76 | } |
| 77 | i, err := v.Lookup(ref...).Fields() |
| 78 | if err != nil { |
| 79 | d.errs = errors.Append(d.errs, errors.Promote(err, "")) |
| 80 | return nil |
| 81 | } |
| 82 | for i.Next() { |
| 83 | ref := append(ref, i.Label()) |
Marcel van Lohuizen | 435989a | 2020-05-06 18:43:58 +0200 | [diff] [blame] | 84 | lab := d.mapRef(i.Value().Pos(), "", ref) |
| 85 | if len(lab) == 0 { |
Marcel van Lohuizen | 671b956 | 2020-03-10 12:42:45 +0100 | [diff] [blame] | 86 | return nil |
| 87 | } |
Marcel van Lohuizen | 435989a | 2020-05-06 18:43:58 +0200 | [diff] [blame] | 88 | decls := d.schema(lab, i.Value()) |
Marcel van Lohuizen | 671b956 | 2020-03-10 12:42:45 +0100 | [diff] [blame] | 89 | a = append(a, decls...) |
| 90 | } |
Marcel van Lohuizen | bd98b13 | 2020-03-05 15:38:21 +0100 | [diff] [blame] | 91 | } |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 92 | |
| 93 | var imports []string |
| 94 | for k := range d.imports { |
| 95 | imports = append(imports, k) |
| 96 | } |
| 97 | sort.Strings(imports) |
| 98 | |
| 99 | if len(imports) > 0 { |
| 100 | x := &ast.ImportDecl{} |
| 101 | for _, p := range imports { |
| 102 | x.Specs = append(x.Specs, ast.NewImport(nil, p)) |
| 103 | } |
Marcel van Lohuizen | 671b956 | 2020-03-10 12:42:45 +0100 | [diff] [blame] | 104 | f.Decls = append(f.Decls, x) |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 105 | } |
| 106 | |
Marcel van Lohuizen | 671b956 | 2020-03-10 12:42:45 +0100 | [diff] [blame] | 107 | f.Decls = append(f.Decls, a...) |
| 108 | f.Decls = append(f.Decls, d.definitions...) |
| 109 | |
| 110 | return f |
| 111 | } |
| 112 | |
Marcel van Lohuizen | 435989a | 2020-05-06 18:43:58 +0200 | [diff] [blame] | 113 | func (d *decoder) schema(ref []ast.Label, v cue.Value) (a []ast.Decl) { |
Marcel van Lohuizen | 671b956 | 2020-03-10 12:42:45 +0100 | [diff] [blame] | 114 | root := state{decoder: d} |
| 115 | |
Marcel van Lohuizen | 435989a | 2020-05-06 18:43:58 +0200 | [diff] [blame] | 116 | var name ast.Label |
Marcel van Lohuizen | 671b956 | 2020-03-10 12:42:45 +0100 | [diff] [blame] | 117 | inner := len(ref) - 1 |
Marcel van Lohuizen | 435989a | 2020-05-06 18:43:58 +0200 | [diff] [blame] | 118 | |
| 119 | if inner >= 0 { |
| 120 | name = ref[inner] |
| 121 | root.isSchema = true |
| 122 | } |
Marcel van Lohuizen | 671b956 | 2020-03-10 12:42:45 +0100 | [diff] [blame] | 123 | |
Marcel van Lohuizen | 78e21c1 | 2020-04-14 15:24:34 +0200 | [diff] [blame] | 124 | expr, state := root.schemaState(v, allTypes, false) |
Marcel van Lohuizen | 671b956 | 2020-03-10 12:42:45 +0100 | [diff] [blame] | 125 | |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 126 | tags := []string{} |
| 127 | if state.jsonschema != "" { |
| 128 | tags = append(tags, fmt.Sprintf("schema=%q", state.jsonschema)) |
| 129 | } |
| 130 | if state.id != "" { |
| 131 | tags = append(tags, fmt.Sprintf("id=%q", state.id)) |
| 132 | } |
Marcel van Lohuizen | 435989a | 2020-05-06 18:43:58 +0200 | [diff] [blame] | 133 | |
| 134 | if name == nil { |
| 135 | if len(tags) > 0 { |
| 136 | body := strings.Join(tags, ",") |
| 137 | a = append(a, &ast.Attribute{ |
| 138 | Text: fmt.Sprintf("@jsonschema(%s)", body)}) |
| 139 | } |
| 140 | |
| 141 | if state.deprecated { |
| 142 | a = append(a, &ast.Attribute{Text: "@deprecated()"}) |
| 143 | } |
| 144 | } else { |
| 145 | if len(tags) > 0 { |
| 146 | a = append(a, addTag(name, "jsonschema", strings.Join(tags, ","))) |
| 147 | } |
| 148 | |
| 149 | if state.deprecated { |
| 150 | a = append(a, addTag(name, "deprecated", "")) |
| 151 | } |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 152 | } |
| 153 | |
Marcel van Lohuizen | 435989a | 2020-05-06 18:43:58 +0200 | [diff] [blame] | 154 | if name != nil { |
| 155 | f := &ast.Field{ |
| 156 | Label: name, |
| 157 | Value: expr, |
| 158 | } |
| 159 | |
| 160 | a = append(a, f) |
| 161 | } else if st, ok := expr.(*ast.StructLit); ok { |
| 162 | a = append(a, st.Elts...) |
| 163 | } else { |
| 164 | a = append(a, &ast.EmbedDecl{Expr: expr}) |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 165 | } |
| 166 | |
Marcel van Lohuizen | 671b956 | 2020-03-10 12:42:45 +0100 | [diff] [blame] | 167 | state.doc(a[0]) |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 168 | |
Marcel van Lohuizen | 671b956 | 2020-03-10 12:42:45 +0100 | [diff] [blame] | 169 | for i := inner - 1; i >= 0; i-- { |
| 170 | a = []ast.Decl{&ast.Field{ |
Marcel van Lohuizen | 435989a | 2020-05-06 18:43:58 +0200 | [diff] [blame] | 171 | Label: ref[i], |
Marcel van Lohuizen | 671b956 | 2020-03-10 12:42:45 +0100 | [diff] [blame] | 172 | Value: &ast.StructLit{Elts: a}, |
| 173 | }} |
Marcel van Lohuizen | 435989a | 2020-05-06 18:43:58 +0200 | [diff] [blame] | 174 | expr = ast.NewStruct(ref[i], expr) |
Marcel van Lohuizen | 671b956 | 2020-03-10 12:42:45 +0100 | [diff] [blame] | 175 | } |
| 176 | |
| 177 | return a |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 178 | } |
| 179 | |
| 180 | func (d *decoder) errf(n cue.Value, format string, args ...interface{}) ast.Expr { |
Marcel van Lohuizen | 671b956 | 2020-03-10 12:42:45 +0100 | [diff] [blame] | 181 | d.warnf(n.Pos(), format, args...) |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 182 | return &ast.BadExpr{From: n.Pos()} |
| 183 | } |
| 184 | |
Marcel van Lohuizen | 671b956 | 2020-03-10 12:42:45 +0100 | [diff] [blame] | 185 | func (d *decoder) warnf(p token.Pos, format string, args ...interface{}) { |
| 186 | d.addErr(errors.Newf(p, format, args...)) |
| 187 | } |
| 188 | |
| 189 | func (d *decoder) addErr(err errors.Error) { |
| 190 | d.errs = errors.Append(d.errs, err) |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 191 | } |
| 192 | |
| 193 | func (d *decoder) number(n cue.Value) ast.Expr { |
Marcel van Lohuizen | f4649b2 | 2020-02-28 16:50:45 +0100 | [diff] [blame] | 194 | return n.Syntax(cue.Final()).(ast.Expr) |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 195 | } |
| 196 | |
| 197 | func (d *decoder) uint(n cue.Value) ast.Expr { |
| 198 | _, err := n.Uint64() |
| 199 | if err != nil { |
| 200 | d.errf(n, "invalid uint") |
| 201 | } |
Marcel van Lohuizen | f4649b2 | 2020-02-28 16:50:45 +0100 | [diff] [blame] | 202 | return n.Syntax(cue.Final()).(ast.Expr) |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 203 | } |
| 204 | |
| 205 | func (d *decoder) bool(n cue.Value) ast.Expr { |
Marcel van Lohuizen | f4649b2 | 2020-02-28 16:50:45 +0100 | [diff] [blame] | 206 | return n.Syntax(cue.Final()).(ast.Expr) |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 207 | } |
| 208 | |
| 209 | func (d *decoder) boolValue(n cue.Value) bool { |
| 210 | x, err := n.Bool() |
| 211 | if err != nil { |
| 212 | d.errf(n, "invalid bool") |
| 213 | } |
| 214 | return x |
| 215 | } |
| 216 | |
| 217 | func (d *decoder) string(n cue.Value) ast.Expr { |
Marcel van Lohuizen | f4649b2 | 2020-02-28 16:50:45 +0100 | [diff] [blame] | 218 | return n.Syntax(cue.Final()).(ast.Expr) |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 219 | } |
| 220 | |
| 221 | func (d *decoder) strValue(n cue.Value) (s string, ok bool) { |
| 222 | s, err := n.String() |
| 223 | if err != nil { |
| 224 | d.errf(n, "invalid string") |
| 225 | return "", false |
| 226 | } |
| 227 | return s, true |
| 228 | } |
| 229 | |
| 230 | // const draftCutoff = 5 |
| 231 | |
| 232 | type state struct { |
| 233 | *decoder |
| 234 | |
Marcel van Lohuizen | 435989a | 2020-05-06 18:43:58 +0200 | [diff] [blame] | 235 | isSchema bool // for omitting ellipsis in an ast.File |
| 236 | |
Marcel van Lohuizen | 78e21c1 | 2020-04-14 15:24:34 +0200 | [diff] [blame] | 237 | parent *state |
| 238 | |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 239 | path []string |
| 240 | |
| 241 | pos cue.Value |
| 242 | |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 243 | typeOptional bool |
Marcel van Lohuizen | 78e21c1 | 2020-04-14 15:24:34 +0200 | [diff] [blame] | 244 | usedTypes cue.Kind |
| 245 | allowedTypes cue.Kind |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 246 | |
| 247 | default_ ast.Expr |
| 248 | examples []ast.Expr |
| 249 | title string |
| 250 | description string |
| 251 | deprecated bool |
| 252 | jsonschema string |
| 253 | id string |
| 254 | |
| 255 | conjuncts []ast.Expr |
| 256 | |
| 257 | obj *ast.StructLit |
| 258 | closeStruct bool |
| 259 | patterns []ast.Expr |
| 260 | |
| 261 | list *ast.ListLit |
| 262 | } |
| 263 | |
Marcel van Lohuizen | 78e21c1 | 2020-04-14 15:24:34 +0200 | [diff] [blame] | 264 | func (s *state) hasConstraints() bool { |
| 265 | return len(s.conjuncts) > 0 || |
| 266 | len(s.patterns) > 0 || |
| 267 | s.title != "" || |
| 268 | s.description != "" || |
Marcel van Lohuizen | b99d364 | 2020-04-14 16:26:33 +0200 | [diff] [blame] | 269 | s.obj != nil |
Marcel van Lohuizen | 78e21c1 | 2020-04-14 15:24:34 +0200 | [diff] [blame] | 270 | } |
| 271 | |
| 272 | const allTypes = cue.NullKind | cue.BoolKind | cue.NumberKind | cue.IntKind | |
| 273 | cue.StringKind | cue.ListKind | cue.StructKind |
| 274 | |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 275 | // finalize constructs a CUE type from the collected constraints. |
| 276 | func (s *state) finalize() (e ast.Expr) { |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 277 | conjuncts := []ast.Expr{} |
| 278 | disjuncts := []ast.Expr{} |
Marcel van Lohuizen | 78e21c1 | 2020-04-14 15:24:34 +0200 | [diff] [blame] | 279 | |
| 280 | add := func(e ast.Expr) { |
| 281 | disjuncts = append(disjuncts, e) // TODO: use setPos |
| 282 | } |
| 283 | |
| 284 | types := s.allowedTypes &^ s.usedTypes |
| 285 | if types == allTypes { |
| 286 | add(ast.NewIdent("_")) |
| 287 | types = 0 |
| 288 | } |
| 289 | if types&cue.FloatKind != 0 { |
| 290 | add(ast.NewIdent("number")) |
| 291 | types &^= cue.IntKind |
| 292 | } |
| 293 | for types != 0 { |
| 294 | k := cue.Kind(1 << uint(bits.TrailingZeros(uint(types)))) |
| 295 | types &^= k |
| 296 | |
| 297 | switch k { |
| 298 | case cue.NullKind: |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 299 | // TODO: handle OpenAPI restrictions. |
| 300 | add(ast.NewIdent("null")) |
Marcel van Lohuizen | 78e21c1 | 2020-04-14 15:24:34 +0200 | [diff] [blame] | 301 | case cue.BoolKind: |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 302 | add(ast.NewIdent("bool")) |
Marcel van Lohuizen | 78e21c1 | 2020-04-14 15:24:34 +0200 | [diff] [blame] | 303 | case cue.StringKind: |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 304 | add(ast.NewIdent("string")) |
Marcel van Lohuizen | 78e21c1 | 2020-04-14 15:24:34 +0200 | [diff] [blame] | 305 | case cue.IntKind: |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 306 | add(ast.NewIdent("int")) |
Marcel van Lohuizen | 78e21c1 | 2020-04-14 15:24:34 +0200 | [diff] [blame] | 307 | case cue.ListKind: |
| 308 | add(ast.NewList(&ast.Ellipsis{})) |
| 309 | case cue.StructKind: |
Marcel van Lohuizen | b236d4a | 2020-02-07 13:14:49 +0100 | [diff] [blame] | 310 | add(ast.NewStruct(&ast.Ellipsis{})) |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 311 | } |
| 312 | } |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 313 | |
| 314 | conjuncts = append(conjuncts, s.conjuncts...) |
| 315 | |
| 316 | if s.obj != nil { |
Marcel van Lohuizen | 435989a | 2020-05-06 18:43:58 +0200 | [diff] [blame] | 317 | // TODO: may need to explicitly close. |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 318 | if !s.closeStruct { |
| 319 | s.obj.Elts = append(s.obj.Elts, &ast.Ellipsis{}) |
| 320 | } |
| 321 | conjuncts = append(conjuncts, s.obj) |
| 322 | } |
| 323 | |
Marcel van Lohuizen | 78e21c1 | 2020-04-14 15:24:34 +0200 | [diff] [blame] | 324 | if len(conjuncts) > 0 { |
| 325 | disjuncts = append(disjuncts, |
| 326 | ast.NewBinExpr(token.AND, conjuncts...)) |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 327 | } |
| 328 | |
Marcel van Lohuizen | 78e21c1 | 2020-04-14 15:24:34 +0200 | [diff] [blame] | 329 | if len(disjuncts) == 0 { |
| 330 | e = &ast.BottomLit{} |
| 331 | } else { |
| 332 | e = ast.NewBinExpr(token.OR, disjuncts...) |
| 333 | } |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 334 | |
| 335 | if s.default_ != nil { |
| 336 | // check conditions where default can be skipped. |
| 337 | switch x := s.default_.(type) { |
| 338 | case *ast.ListLit: |
Marcel van Lohuizen | 78e21c1 | 2020-04-14 15:24:34 +0200 | [diff] [blame] | 339 | if s.usedTypes == cue.ListKind && len(x.Elts) == 0 { |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 340 | return e |
| 341 | } |
| 342 | } |
| 343 | e = ast.NewBinExpr(token.OR, e, &ast.UnaryExpr{Op: token.MUL, X: s.default_}) |
| 344 | } |
| 345 | return e |
| 346 | } |
| 347 | |
Marcel van Lohuizen | 78e21c1 | 2020-04-14 15:24:34 +0200 | [diff] [blame] | 348 | func isAny(s ast.Expr) bool { |
| 349 | i, ok := s.(*ast.Ident) |
| 350 | return ok && i.Name == "_" |
| 351 | } |
| 352 | |
Marcel van Lohuizen | bd98b13 | 2020-03-05 15:38:21 +0100 | [diff] [blame] | 353 | func (s *state) comment() *ast.CommentGroup { |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 354 | // Create documentation. |
| 355 | doc := strings.TrimSpace(s.title) |
| 356 | if s.description != "" { |
| 357 | if doc != "" { |
| 358 | doc += "\n\n" |
| 359 | } |
| 360 | doc += s.description |
| 361 | doc = strings.TrimSpace(doc) |
| 362 | } |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 363 | // TODO: add examples as well? |
Marcel van Lohuizen | bd98b13 | 2020-03-05 15:38:21 +0100 | [diff] [blame] | 364 | if doc == "" { |
| 365 | return nil |
| 366 | } |
| 367 | return internal.NewComment(true, doc) |
| 368 | } |
| 369 | |
| 370 | func (s *state) doc(n ast.Node) { |
| 371 | doc := s.comment() |
| 372 | if doc != nil { |
| 373 | ast.SetComments(n, []*ast.CommentGroup{doc}) |
| 374 | } |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 375 | } |
| 376 | |
Marcel van Lohuizen | 78e21c1 | 2020-04-14 15:24:34 +0200 | [diff] [blame] | 377 | func (s *state) addConjunct(e ast.Expr) { |
| 378 | if !isAny(e) { |
| 379 | s.conjuncts = append(s.conjuncts, e) |
| 380 | } |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 381 | } |
| 382 | |
| 383 | func (s *state) schema(n cue.Value) ast.Expr { |
Marcel van Lohuizen | 78e21c1 | 2020-04-14 15:24:34 +0200 | [diff] [blame] | 384 | expr, _ := s.schemaState(n, allTypes, false) |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 385 | // TODO: report unused doc. |
| 386 | return expr |
| 387 | } |
| 388 | |
Marcel van Lohuizen | 78e21c1 | 2020-04-14 15:24:34 +0200 | [diff] [blame] | 389 | // schemaState is a low-level API for schema. isLogical specifies whether the |
| 390 | // caller is a logical operator like anyOf, allOf, oneOf, or not. |
| 391 | func (s *state) schemaState(n cue.Value, types cue.Kind, isLogical bool) (ast.Expr, *state) { |
| 392 | state := &state{ |
Marcel van Lohuizen | 435989a | 2020-05-06 18:43:58 +0200 | [diff] [blame] | 393 | isSchema: s.isSchema, |
Marcel van Lohuizen | 78e21c1 | 2020-04-14 15:24:34 +0200 | [diff] [blame] | 394 | decoder: s.decoder, |
| 395 | allowedTypes: types, |
| 396 | path: s.path, |
| 397 | pos: n, |
| 398 | } |
| 399 | if isLogical { |
| 400 | state.parent = s |
| 401 | } |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 402 | |
| 403 | if n.Kind() != cue.StructKind { |
| 404 | return s.errf(n, "schema expects mapping node, found %s", n.Kind()), state |
| 405 | } |
| 406 | |
| 407 | // do multiple passes over the constraints to ensure they are done in order. |
| 408 | for pass := 0; pass < 3; pass++ { |
| 409 | state.processMap(n, func(key string, value cue.Value) { |
| 410 | // Convert each constraint into a either a value or a functor. |
| 411 | c := constraintMap[key] |
| 412 | if c == nil { |
Marcel van Lohuizen | 0059b2b | 2020-04-14 15:31:11 +0200 | [diff] [blame] | 413 | if pass == 0 && s.cfg.Strict { |
| 414 | // TODO: value is not the correct possition, albeit close. Fix this. |
| 415 | s.warnf(value.Pos(), "unsupported constraint %q", key) |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 416 | } |
| 417 | return |
| 418 | } |
| 419 | if c.phase == pass { |
| 420 | c.fn(value, state) |
| 421 | } |
| 422 | }) |
| 423 | } |
| 424 | |
| 425 | return state.finalize(), state |
| 426 | } |
| 427 | |
| 428 | func (s *state) value(n cue.Value) ast.Expr { |
Marcel van Lohuizen | 78e21c1 | 2020-04-14 15:24:34 +0200 | [diff] [blame] | 429 | k := n.Kind() |
| 430 | s.usedTypes |= k |
| 431 | s.allowedTypes &= k |
| 432 | switch k { |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 433 | case cue.ListKind: |
| 434 | a := []ast.Expr{} |
| 435 | for i, _ := n.List(); i.Next(); { |
| 436 | a = append(a, s.value(i.Value())) |
| 437 | } |
| 438 | return setPos(ast.NewList(a...), n) |
| 439 | |
| 440 | case cue.StructKind: |
| 441 | a := []ast.Decl{} |
| 442 | s.processMap(n, func(key string, n cue.Value) { |
| 443 | a = append(a, &ast.Field{ |
| 444 | Label: ast.NewString(key), |
| 445 | Value: s.value(n), |
| 446 | }) |
| 447 | }) |
Marcel van Lohuizen | 435989a | 2020-05-06 18:43:58 +0200 | [diff] [blame] | 448 | // TODO: only open when s.isSchema? |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 449 | a = append(a, &ast.Ellipsis{}) |
| 450 | return setPos(&ast.StructLit{Elts: a}, n) |
| 451 | |
| 452 | default: |
| 453 | if !n.IsConcrete() { |
Marcel van Lohuizen | 1370f0a | 2020-04-15 11:27:13 +0200 | [diff] [blame] | 454 | s.errf(n, "invalid non-concrete value") |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 455 | } |
Marcel van Lohuizen | f4649b2 | 2020-02-28 16:50:45 +0100 | [diff] [blame] | 456 | return n.Syntax(cue.Final()).(ast.Expr) |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 457 | } |
| 458 | } |
| 459 | |
| 460 | // processMap processes a yaml node, expanding merges. |
| 461 | // |
| 462 | // TODO: in some cases we can translate merges into CUE embeddings. |
| 463 | // This may also prevent exponential blow-up (as may happen when |
| 464 | // converting YAML to JSON). |
| 465 | func (s *state) processMap(n cue.Value, f func(key string, n cue.Value)) { |
| 466 | saved := s.path |
| 467 | defer func() { s.path = saved }() |
| 468 | |
| 469 | // TODO: intercept references to allow for optimized performance. |
| 470 | for i, _ := n.Fields(); i.Next(); { |
| 471 | key := i.Label() |
| 472 | s.path = append(saved, key) |
| 473 | f(key, i.Value()) |
| 474 | } |
| 475 | } |
| 476 | |
Marcel van Lohuizen | 78e21c1 | 2020-04-14 15:24:34 +0200 | [diff] [blame] | 477 | func (s *state) listItems(name string, n cue.Value, allowEmpty bool) (a []cue.Value) { |
| 478 | if n.Kind() != cue.ListKind { |
| 479 | s.errf(n, `value of %q must be an array, found %v`, name, n.Kind()) |
| 480 | } |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 481 | for i, _ := n.List(); i.Next(); { |
| 482 | a = append(a, i.Value()) |
| 483 | } |
Marcel van Lohuizen | 78e21c1 | 2020-04-14 15:24:34 +0200 | [diff] [blame] | 484 | if !allowEmpty && len(a) == 0 { |
| 485 | s.errf(n, `array for %q must be non-empty`, name) |
| 486 | } |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 487 | return a |
| 488 | } |
| 489 | |
| 490 | // excludeFields returns a CUE expression that can be used to exclude the |
| 491 | // fields of the given declaration in a label expression. For instance, for |
| 492 | // |
| 493 | // { foo: 1, bar: int } |
| 494 | // |
| 495 | // it creates |
| 496 | // |
| 497 | // "^(foo|bar)$" |
| 498 | // |
| 499 | // which can be used in a label expression to define types for all fields but |
| 500 | // those existing: |
| 501 | // |
| 502 | // [!~"^(foo|bar)$"]: string |
| 503 | // |
| 504 | func excludeFields(decls []ast.Decl) ast.Expr { |
| 505 | var a []string |
| 506 | for _, d := range decls { |
| 507 | f, ok := d.(*ast.Field) |
| 508 | if !ok { |
| 509 | continue |
| 510 | } |
| 511 | str, _, _ := ast.LabelName(f.Label) |
| 512 | if str != "" { |
| 513 | a = append(a, str) |
| 514 | } |
| 515 | } |
| 516 | re := fmt.Sprintf("^(%s)$", strings.Join(a, "|")) |
| 517 | return &ast.UnaryExpr{Op: token.NMAT, X: ast.NewString(re)} |
| 518 | } |
| 519 | |
Marcel van Lohuizen | 435989a | 2020-05-06 18:43:58 +0200 | [diff] [blame] | 520 | func addTag(field ast.Label, tag, value string) *ast.Field { |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 521 | return &ast.Field{ |
Marcel van Lohuizen | 435989a | 2020-05-06 18:43:58 +0200 | [diff] [blame] | 522 | Label: field, |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 523 | Value: ast.NewIdent("_"), |
| 524 | Attrs: []*ast.Attribute{ |
Daniel Martà | 066ede6 | 2020-02-11 15:07:27 +0000 | [diff] [blame] | 525 | {Text: fmt.Sprintf("@%s(%s)", tag, value)}, |
Marcel van Lohuizen | 6cb0878 | 2020-01-13 18:03:27 +0100 | [diff] [blame] | 526 | }, |
| 527 | } |
| 528 | } |
| 529 | |
| 530 | func setPos(e ast.Expr, v cue.Value) ast.Expr { |
| 531 | ast.SetPos(e, v.Pos()) |
| 532 | return e |
| 533 | } |