blob: 33cb400ce7ddc7baaaeb8b7ef4991525a398bbf9 [file] [log] [blame]
// Copyright 2019 CUE Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jsonschema
// TODO:
// - replace converter from YAML to CUE to CUE (schema) to CUE.
// - define OpenAPI definitions als CUE.
import (
"fmt"
"path"
"path/filepath"
"sort"
"strings"
"cuelang.org/go/cue"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/errors"
"cuelang.org/go/cue/token"
"cuelang.org/go/internal"
)
const rootDefs = "def"
// A decoder converts JSON schema to CUE.
type decoder struct {
cfg *Config
errs errors.Error
imports map[string]*ast.Ident
definitions []ast.Decl
}
// addImport registers
func (d *decoder) addImport(pkg string) *ast.Ident {
ident, ok := d.imports[pkg]
if !ok {
ident = ast.NewIdent(pkg)
d.imports[pkg] = ident
}
return ident
}
func (d *decoder) decode(inst *cue.Instance) *ast.File {
root := state{decoder: d}
expr, state := root.schemaState(inst.Value())
var a []ast.Decl
filename := d.cfg.ID
name := filepath.ToSlash(filename)
if state.id != "" {
name = strings.Trim(name, "#")
}
pkgName := path.Base(name)
pkgName = pkgName[:len(pkgName)-len(path.Ext(pkgName))]
pkg := &ast.Package{Name: ast.NewIdent(pkgName)}
state.doc(pkg)
a = append(a, pkg)
var imports []string
for k := range d.imports {
imports = append(imports, k)
}
sort.Strings(imports)
if len(imports) > 0 {
x := &ast.ImportDecl{}
for _, p := range imports {
x.Specs = append(x.Specs, ast.NewImport(nil, p))
}
a = append(a, x)
}
tags := []string{}
if state.jsonschema != "" {
tags = append(tags, fmt.Sprintf("schema=%q", state.jsonschema))
}
if state.id != "" {
tags = append(tags, fmt.Sprintf("id=%q", state.id))
}
if len(tags) > 0 {
a = append(a, addTag("Schema", "jsonschema", strings.Join(tags, ",")))
}
if state.deprecated {
a = append(a, addTag("Schema", "deprecated", ""))
}
f := &ast.Field{
Label: ast.NewIdent("Schema"),
Value: expr,
}
f.Token = token.ISA
a = append(a, f)
a = append(a, d.definitions...)
return &ast.File{Decls: a}
}
func (d *decoder) errf(n cue.Value, format string, args ...interface{}) ast.Expr {
d.warnf(n, format, args...)
return &ast.BadExpr{From: n.Pos()}
}
func (d *decoder) warnf(n cue.Value, format string, args ...interface{}) {
d.errs = errors.Append(d.errs, errors.Newf(n.Pos(), format, args...))
}
func (d *decoder) number(n cue.Value) ast.Expr {
return n.Syntax().(ast.Expr)
}
func (d *decoder) uint(n cue.Value) ast.Expr {
_, err := n.Uint64()
if err != nil {
d.errf(n, "invalid uint")
}
return n.Syntax().(ast.Expr)
}
func (d *decoder) bool(n cue.Value) ast.Expr {
return n.Syntax().(ast.Expr)
}
func (d *decoder) boolValue(n cue.Value) bool {
x, err := n.Bool()
if err != nil {
d.errf(n, "invalid bool")
}
return x
}
func (d *decoder) string(n cue.Value) ast.Expr {
return n.Syntax().(ast.Expr)
}
func (d *decoder) strValue(n cue.Value) (s string, ok bool) {
s, err := n.String()
if err != nil {
d.errf(n, "invalid string")
return "", false
}
return s, true
}
// const draftCutoff = 5
type state struct {
*decoder
path []string
pos cue.Value
types []cue.Value
typeOptional bool
kind cue.Kind
default_ ast.Expr
examples []ast.Expr
title string
description string
deprecated bool
jsonschema string
id string
conjuncts []ast.Expr
obj *ast.StructLit
closeStruct bool
patterns []ast.Expr
list *ast.ListLit
}
// finalize constructs a CUE type from the collected constraints.
func (s *state) finalize() (e ast.Expr) {
if s.typeOptional || s.kind != 0 {
if len(s.types) > 1 {
s.errf(s.pos, "constraints require specific type")
}
s.types = nil
}
conjuncts := []ast.Expr{}
disjuncts := []ast.Expr{}
for _, n := range s.types {
add := func(e ast.Expr) {
disjuncts = append(disjuncts, setPos(e, n))
}
str, ok := s.strValue(n)
if !ok {
s.errf(n, "type value should be a string")
return
}
switch str {
case "null":
// TODO: handle OpenAPI restrictions.
add(ast.NewIdent("null"))
case "boolean":
add(ast.NewIdent("bool"))
case "string":
add(ast.NewIdent("string"))
case "number":
add(ast.NewIdent("number"))
case "integer":
add(ast.NewIdent("int"))
case "array":
if s.kind&cue.ListKind == 0 {
add(ast.NewList(&ast.Ellipsis{}))
}
case "object":
add(ast.NewStruct(&ast.Ellipsis{}))
default:
s.errf(n, "unknown type %q", n)
}
}
if len(disjuncts) > 0 {
conjuncts = append(conjuncts, ast.NewBinExpr(token.OR, disjuncts...))
}
conjuncts = append(conjuncts, s.conjuncts...)
if s.obj != nil {
if !s.closeStruct {
s.obj.Elts = append(s.obj.Elts, &ast.Ellipsis{})
}
conjuncts = append(conjuncts, s.obj)
}
if len(conjuncts) == 0 {
return ast.NewString(fmt.Sprint(s.pos))
}
e = ast.NewBinExpr(token.AND, conjuncts...)
if s.default_ != nil {
// check conditions where default can be skipped.
switch x := s.default_.(type) {
case *ast.ListLit:
if s.kind == cue.ListKind && len(x.Elts) == 0 {
return e
}
}
e = ast.NewBinExpr(token.OR, e, &ast.UnaryExpr{Op: token.MUL, X: s.default_})
}
return e
}
func (s *state) doc(n ast.Node) {
// Create documentation.
doc := strings.TrimSpace(s.title)
if s.description != "" {
if doc != "" {
doc += "\n\n"
}
doc += s.description
doc = strings.TrimSpace(doc)
}
if doc != "" {
ast.SetComments(n, []*ast.CommentGroup{
internal.NewComment(true, doc),
})
}
// TODO: add examples as well?
}
func (s *state) add(e ast.Expr) {
s.conjuncts = append(s.conjuncts, e)
}
func (s *state) schema(n cue.Value) ast.Expr {
expr, _ := s.schemaState(n)
// TODO: report unused doc.
return expr
}
func (s *state) schemaState(n cue.Value) (ast.Expr, *state) {
state := &state{path: s.path, pos: n, decoder: s.decoder}
if n.Kind() != cue.StructKind {
return s.errf(n, "schema expects mapping node, found %s", n.Kind()), state
}
// do multiple passes over the constraints to ensure they are done in order.
for pass := 0; pass < 3; pass++ {
state.processMap(n, func(key string, value cue.Value) {
// Convert each constraint into a either a value or a functor.
c := constraintMap[key]
if c == nil {
if pass == 0 {
s.warnf(n, "unsupported constraint %q", key)
}
return
}
if c.phase == pass {
c.fn(value, state)
}
})
}
return state.finalize(), state
}
func (s *state) value(n cue.Value) ast.Expr {
switch n.Kind() {
case cue.ListKind:
a := []ast.Expr{}
for i, _ := n.List(); i.Next(); {
a = append(a, s.value(i.Value()))
}
return setPos(ast.NewList(a...), n)
case cue.StructKind:
a := []ast.Decl{}
s.processMap(n, func(key string, n cue.Value) {
a = append(a, &ast.Field{
Label: ast.NewString(key),
Value: s.value(n),
})
})
a = append(a, &ast.Ellipsis{})
return setPos(&ast.StructLit{Elts: a}, n)
default:
if !n.IsConcrete() {
s.errf(n, "invalid non-concerte value")
}
return n.Syntax().(ast.Expr)
}
}
// processMap processes a yaml node, expanding merges.
//
// TODO: in some cases we can translate merges into CUE embeddings.
// This may also prevent exponential blow-up (as may happen when
// converting YAML to JSON).
func (s *state) processMap(n cue.Value, f func(key string, n cue.Value)) {
saved := s.path
defer func() { s.path = saved }()
// TODO: intercept references to allow for optimized performance.
for i, _ := n.Fields(); i.Next(); {
key := i.Label()
s.path = append(saved, key)
f(key, i.Value())
}
}
func list(n cue.Value) (a []cue.Value) {
for i, _ := n.List(); i.Next(); {
a = append(a, i.Value())
}
return a
}
// excludeFields returns a CUE expression that can be used to exclude the
// fields of the given declaration in a label expression. For instance, for
//
// { foo: 1, bar: int }
//
// it creates
//
// "^(foo|bar)$"
//
// which can be used in a label expression to define types for all fields but
// those existing:
//
// [!~"^(foo|bar)$"]: string
//
func excludeFields(decls []ast.Decl) ast.Expr {
var a []string
for _, d := range decls {
f, ok := d.(*ast.Field)
if !ok {
continue
}
str, _, _ := ast.LabelName(f.Label)
if str != "" {
a = append(a, str)
}
}
re := fmt.Sprintf("^(%s)$", strings.Join(a, "|"))
return &ast.UnaryExpr{Op: token.NMAT, X: ast.NewString(re)}
}
func addTag(field, tag, value string) *ast.Field {
return &ast.Field{
Label: ast.NewIdent(field),
Token: token.ISA,
Value: ast.NewIdent("_"),
Attrs: []*ast.Attribute{
{Text: fmt.Sprintf("@%s(%s)", tag, value)},
},
}
}
func setPos(e ast.Expr, v cue.Value) ast.Expr {
ast.SetPos(e, v.Pos())
return e
}