diff --git a/encoding/openapi/openapi.go b/encoding/openapi/openapi.go index 9d7d5e7a4..c714d2126 100644 --- a/encoding/openapi/openapi.go +++ b/encoding/openapi/openapi.go @@ -99,7 +99,11 @@ func Gen(inst cue.InstanceOrValue, c *Config) ([]byte, error) { if err != nil { return nil, err } - return internaljson.Marshal((*OrderedMap)(top)) + topValue := inst.Value().Context().BuildExpr(top) + if err := topValue.Err(); err != nil { + return nil, err + } + return internaljson.Marshal(topValue) } // Generate generates the set of OpenAPI schema for all top-level types of the diff --git a/encoding/openapi/orderedmap.go b/encoding/openapi/orderedmap.go index 774625a86..18bf01332 100644 --- a/encoding/openapi/orderedmap.go +++ b/encoding/openapi/orderedmap.go @@ -16,11 +16,10 @@ package openapi import ( "cuelang.org/go/cue/ast" - internaljson "cuelang.org/go/internal/encoding/json" ) // An OrderedMap is a set of key-value pairs that preserves the order in which -// items were added. It marshals to JSON as an object. +// items were added. // // Deprecated: the API now returns an ast.File. This allows OpenAPI to be // represented as JSON, YAML, or CUE data, in addition to being able to use @@ -94,10 +93,3 @@ func (m *OrderedMap) getMap(key string) *OrderedMap { } return (*OrderedMap)(f.Value.(*ast.StructLit)) } - -// MarshalJSON implements [encoding/json.Marshaler]. -func (m *OrderedMap) MarshalJSON() (b []byte, err error) { - // This is a pointer receiever to enforce that we only store pointers to - // OrderedMap in the output. - return internaljson.Encode((*ast.StructLit)(m)) -} diff --git a/internal/encoding/json/encode.go b/internal/encoding/json/encode.go index 40c2efdb9..7d6a11dcd 100644 --- a/internal/encoding/json/encode.go +++ b/internal/encoding/json/encode.go @@ -17,15 +17,6 @@ package json import ( "bytes" "encoding/json" - "math/big" - "strings" - - "cuelang.org/go/cue/ast" - "cuelang.org/go/cue/errors" - "cuelang.org/go/cue/literal" - "cuelang.org/go/cue/token" - "cuelang.org/go/internal" - "cuelang.org/go/internal/astinternal" ) // Marshal is a replacement for [json.Marshal] without HTML escaping. @@ -41,282 +32,3 @@ func Marshal(v any) ([]byte, error) { p = bytes.TrimSuffix(p, []byte("\n")) return p, nil } - -// Encode converts a CUE AST to unescaped JSON. -// -// The given file must only contain values that can be directly supported by -// JSON: -// -// Type Restrictions -// BasicLit -// File no imports, aliases, or definitions -// StructLit no embeddings, aliases, or definitions -// List -// Field must be regular; label must be a BasicLit or Ident -// -// Comments and attributes are ignored. -func Encode(n ast.Node) (b []byte, err error) { - e := encoder{} - err = e.encode(n) - if err != nil { - return nil, err - } - return e.w.Bytes(), nil -} - -type encoder struct { - w bytes.Buffer - tab []byte - indentsAtLevel []int - indenting bool - unIndenting int -} - -func (e *encoder) writeIndent(b byte) { - if e.indenting { - e.indentsAtLevel[len(e.indentsAtLevel)-1]++ - } else { - e.indentsAtLevel = append(e.indentsAtLevel, 0) - } - e.indenting = true - _ = e.w.WriteByte(b) -} - -func (e *encoder) writeUnindent(b byte, pos, def token.Pos) { - if e.unIndenting > 0 { - e.unIndenting-- - } else { - e.unIndenting = e.indentsAtLevel[len(e.indentsAtLevel)-1] - e.indentsAtLevel = e.indentsAtLevel[:len(e.indentsAtLevel)-1] - } - e.indenting = false - e.ws(pos, def.RelPos()) - _ = e.w.WriteByte(b) -} - -func (e *encoder) writeString(s string) { - _, _ = e.w.WriteString(s) - e.indenting = false -} - -func (e *encoder) writeByte(b byte) { - _ = e.w.WriteByte(b) -} - -func (e *encoder) write(b []byte) { - _, _ = e.w.Write(b) - e.indenting = false -} - -func (e *encoder) indent() { - for range e.indentsAtLevel { - e.write(e.tab) - } -} - -func (e *encoder) ws(pos token.Pos, default_ token.RelPos) { - rel := pos.RelPos() - if pos == token.NoPos { - rel = default_ - } - switch rel { - case token.NoSpace: - case token.Blank: - e.writeByte(' ') - case token.Newline: - e.writeByte('\n') - e.indent() - case token.NewSection: - e.writeString("\n\n") - e.indent() - } -} -func (e *encoder) encode(n ast.Node) error { - if e.tab == nil { - e.tab = []byte(" ") - } - const defPos = token.NoSpace - switch x := n.(type) { - case *ast.BasicLit: - e.ws(x.Pos(), defPos) - return e.encodeScalar(x, true) - - case *ast.ListLit: - e.ws(foldNewline(x.Pos()), token.NoRelPos) - if len(x.Elts) == 0 { - e.writeString("[]") - return nil - } - e.writeIndent('[') - for i, x := range x.Elts { - if i > 0 { - e.writeString(",") - } - if err := e.encode(x); err != nil { - return err - } - } - e.writeUnindent(']', x.Rbrack, compactNewline(x.Elts[0].Pos())) - return nil - - case *ast.StructLit: - e.ws(foldNewline(n.Pos()), token.NoRelPos) - return e.encodeDecls(x.Elts, x.Rbrace) - - case *ast.File: - return e.encodeDecls(x.Decls, token.NoPos) - - case *ast.UnaryExpr: - e.ws(foldNewline(x.Pos()), defPos) - l, ok := x.X.(*ast.BasicLit) - if ok && x.Op == token.SUB && (l.Kind == token.INT || l.Kind == token.FLOAT) { - e.writeByte('-') - return e.encodeScalar(l, false) - } - } - return errors.Newf(n.Pos(), "json: unsupported node %s (%T)", astinternal.DebugStr(n), n) -} - -func (e *encoder) encodeScalar(l *ast.BasicLit, allowMinus bool) error { - switch l.Kind { - case token.INT: - var x big.Int - return e.setNum(l, allowMinus, &x) - - case token.FLOAT: - var x big.Float - return e.setNum(l, allowMinus, &x) - - case token.TRUE: - e.writeString("true") - - case token.FALSE: - e.writeString("false") - - case token.NULL: - e.writeString("null") - - case token.STRING: - str, err := literal.Unquote(l.Value) - if err != nil { - return err - } - b, err := Marshal(str) - if err != nil { - return err - } - e.write(b) - - default: - return errors.Newf(l.Pos(), "unknown literal type %v", l.Kind) - } - return nil -} - -func (e *encoder) setNum(l *ast.BasicLit, allowMinus bool, x interface{}) error { - if !allowMinus && strings.HasPrefix(l.Value, "-") { - return errors.Newf(l.Pos(), "double minus not allowed") - } - var ni literal.NumInfo - if err := literal.ParseNum(l.Value, &ni); err != nil { - return err - } - e.writeString(ni.String()) - return nil -} - -// encodeDecls converts a sequence of declarations to a value. If it encounters -// an embedded value, it will return this expression. This is more relaxed for -// structs than is currently allowed for CUE, but the expectation is that this -// will be allowed at some point. The input would still be illegal CUE. -func (e *encoder) encodeDecls(decls []ast.Decl, endPos token.Pos) error { - var embed ast.Expr - var fields []*ast.Field - - for _, d := range decls { - switch x := d.(type) { - default: - return errors.Newf(x.Pos(), "json: unsupported node %s (%T)", astinternal.DebugStr(x), x) - - case *ast.Package: - if embed != nil || fields != nil { - return errors.Newf(x.Pos(), "invalid package clause") - } - continue - - case *ast.Field: - if !internal.IsRegularField(x) { - return errors.Newf(x.TokenPos, "json: definition or hidden field not allowed") - } - if x.Optional != token.NoPos { - return errors.Newf(x.Optional, "json: optional fields not allowed") - } - fields = append(fields, x) - - case *ast.EmbedDecl: - if embed != nil { - return errors.Newf(x.Pos(), "json: multiple embedded values") - } - embed = x.Expr - - case *ast.CommentGroup: - } - } - - if embed != nil { - if fields != nil { - return errors.Newf(embed.Pos(), "json: embedding mixed with fields") - } - return e.encode(embed) - } - - if len(fields) == 0 { - e.writeString("{}") - return nil - } - - e.writeIndent('{') - pos := compactNewline(fields[0].Pos()) - if endPos == token.NoPos && pos.RelPos() == token.Blank { - pos = token.NoPos - } - firstPos := pos - const defPos = token.NoRelPos - for i, x := range fields { - if i > 0 { - e.writeByte(',') - pos = x.Pos() - } - name, _, err := ast.LabelName(x.Label) - if err != nil { - return errors.Newf(x.Label.Pos(), "json: only literal labels allowed") - } - b, err := Marshal(name) - if err != nil { - return err - } - e.ws(pos, defPos) - e.write(b) - e.writeByte(':') - - if err := e.encode(x.Value); err != nil { - return err - } - } - e.writeUnindent('}', endPos, firstPos) - return nil -} - -func compactNewline(pos token.Pos) token.Pos { - if pos.RelPos() == token.NewSection { - pos = token.Newline.Pos() - } - return pos -} - -func foldNewline(pos token.Pos) token.Pos { - if pos.RelPos() >= token.Newline { - pos = token.Blank.Pos() - } - return pos -} diff --git a/internal/encoding/json/encode_test.go b/internal/encoding/json/encode_test.go deleted file mode 100644 index 0bf00d0c8..000000000 --- a/internal/encoding/json/encode_test.go +++ /dev/null @@ -1,315 +0,0 @@ -// Copyright 2020 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 json - -import ( - "encoding/json" - "strings" - "testing" - - "cuelang.org/go/cue/ast" - "cuelang.org/go/cue/parser" - "cuelang.org/go/cue/token" - "github.com/google/go-cmp/cmp" -) - -func TestEncodeFile(t *testing.T) { - testCases := []struct { - name string - in string - out string - }{{ - name: "foo", - in: ` - package test - - seq: [ - 1, 2, 3, { - a: 1 - b: 2 - } - ] - a: b: c: 3 - b: { - x: 0 - y: 1 - z: 2 - } - `, - out: `{ - "seq": [ - 1, 2, 3, { - "a": 1, - "b": 2 - } - ], - "a": {"b": {"c": 3}}, - "b": { - "x": 0, - "y": 1, - "z": 2 - } -}`, - }, { - name: "oneLineFields", - in: ` - seq: [1, 2, 3] - esq: [] - emp: {} - map: {a: 3} - str: "str" - int: 1K - bin: 0b11 - hex: 0x11 - dec: .3 - dat: '\x80' - nil: null - yes: true - non: false - `, - out: `{ - "seq": [1, 2, 3], - "esq": [], - "emp": {}, - "map": {"a": 3}, - "str": "str", - "int": 1000, - "bin": 3, - "hex": 17, - "dec": 0.3, - "dat": "\ufffd", - "nil": null, - "yes": true, - "non": false -}`, - }, { - name: "comments", - in: ` -// Document - -// head 1 -f1: 1 -// foot 1 - -// head 2 -f2: 2 // line 2 - -// intermezzo f2 -// -// with multiline - -// head 3 -f3: - // struct doc - { - a: 1 - } - -f4: { -} // line 4 - -// Trailing -`, - out: `{ - "f1": 1, - "f2": 2, - "f3": { - "a": 1 - }, - - "f4": {} -}`, - }, { - // TODO: support this at some point - name: "embed", - in: ` - // hex - 0xabc // line - // trail - `, - out: `2748`, - }, { - name: "anchors", - in: ` - a: b - b: 3 - `, - out: "json: unsupported node b (*ast.Ident)", - }, { - name: "errors", - in: ` - m: { - a: 1 - b: 3 - } - c: [1, [for x in m { x }]] - `, - out: "json: unsupported node for x in m {x} (*ast.Comprehension)", - }, { - name: "disallowMultipleEmbeddings", - in: ` - 1 - 1 - `, - out: "json: multiple embedded values", - }, { - name: "disallowDefinitions", - in: `#a: 2 `, - out: "json: definition or hidden field not allowed", - }, { - name: "disallowHidden", - in: `_a: 2 `, - out: "json: definition or hidden field not allowed", - }, { - name: "disallowOptionals", - in: `a?: 2`, - out: "json: optional fields not allowed", - }, { - name: "disallowBulkOptionals", - in: `[string]: 2`, - out: "json: only literal labels allowed", - }, { - name: "noImports", - in: ` - import "foo" - - a: 1 - `, - out: `json: unsupported node import "foo" (*ast.ImportDecl)`, - }, { - name: "disallowMultipleEmbeddings", - in: ` - 1 - a: 2 - `, - out: "json: embedding mixed with fields", - }, { - name: "prometheus", - in: ` - { - receivers: [{ - name: "pager" - slack_configs: [{ - text: """ - {{ range .Alerts }}{{ .Annotations.description }} - {{ end }} - """ - channel: "#cloudmon" - send_resolved: true - }] - }] - route: { - receiver: "pager" - group_by: ["alertname", "cluster"] - } - }`, - out: `{ - "receivers": [{ - "name": "pager", - "slack_configs": [{ - "text": "{{ range .Alerts }}{{ .Annotations.description }}\n{{ end }}", - "channel": "#cloudmon", - "send_resolved": true - }] - }], - "route": { - "receiver": "pager", - "group_by": ["alertname", "cluster"] - } -}`, - }} - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - f, err := parser.ParseFile(tc.name, tc.in, parser.ParseComments) - if err != nil { - t.Fatal(err) - } - b, err := Encode(f) - var got string - if err != nil { - got = err.Error() - } else { - if !json.Valid(b) { - t.Fatal("invalid JSON") - } - got = strings.TrimSpace(string(b)) - } - want := strings.TrimSpace(tc.out) - if got != want { - t.Log("\n" + got) - t.Error(cmp.Diff(got, want)) - } - }) - } -} - -func TestEncodeAST(t *testing.T) { - comment := func(s string) *ast.CommentGroup { - return &ast.CommentGroup{List: []*ast.Comment{ - {Text: "// " + s}, - }} - } - testCases := []struct { - name string - in ast.Expr - out string - }{{ - in: ast.NewStruct( - comment("foo"), - comment("bar"), - "field", ast.NewString("value"), - "field2", ast.NewString("value"), - comment("trail1"), - comment("trail2"), - ), - out: `{"field":"value","field2":"value"}`, - }, { - in: &ast.StructLit{Elts: []ast.Decl{ - comment("bar"), - &ast.EmbedDecl{Expr: ast.NewBool(true)}, - }}, - out: `true`, - }, { - in: &ast.UnaryExpr{ - Op: token.SUB, - X: &ast.BasicLit{Kind: token.INT, Value: "-2"}, - }, - out: `double minus not allowed`, - }, { - in: &ast.BasicLit{Kind: token.INT, Value: "-2.0.0"}, - out: `invalid number "-2.0.0"`, - }, { - in: &ast.StructLit{Elts: []ast.Decl{ - &ast.EmbedDecl{Expr: ast.NewBool(true)}, - &ast.Package{}, - }}, - out: `invalid package clause`, - }} - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - b, err := Encode(tc.in) - var got string - if err != nil { - got = err.Error() - } else { - got = strings.TrimSpace(string(b)) - } - want := strings.TrimSpace(tc.out) - if got != want { - t.Error(cmp.Diff(got, want)) - } - }) - } -}