Skip to content

Commit e7d9d47

Browse files
committed
encoding/openapi: marshal JSON via cue.Value.MarshalJSON
Rather than via internal/encoding/json.Encode, which was an entirely separate implementation of a CUE to JSON marshaler working on cue/ast rather than cue.Value. We can use the cue.Value API by building the AST, which should not be an expensive operation given how the syntax trees describe concrete CUE values which can be encoded as JSON. There are several reasons to want to remove the cue/ast json encoder: * We do not want to maintain two JSON encoding implementations * cue.Value.MarshalJSON is more widely used, e.g. when exporting to JSON * We are converging towards Encode/Marshal APIs taking cue.Value rather than AST nodes, given that the former is much more flexible Signed-off-by: Daniel Martí <[email protected]> Change-Id: Id2f5f02579f4cbc1e94a263035624bc5781b5587 Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1201935 Unity-Result: CUE porcuepine <[email protected]> Reviewed-by: Roger Peppe <[email protected]> TryBot-Result: CUEcueckoo <[email protected]>
1 parent 90c9c95 commit e7d9d47

File tree

4 files changed

+6
-613
lines changed

4 files changed

+6
-613
lines changed

encoding/openapi/openapi.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,11 @@ func Gen(inst cue.InstanceOrValue, c *Config) ([]byte, error) {
9999
if err != nil {
100100
return nil, err
101101
}
102-
return internaljson.Marshal((*OrderedMap)(top))
102+
topValue := inst.Value().Context().BuildExpr(top)
103+
if err := topValue.Err(); err != nil {
104+
return nil, err
105+
}
106+
return internaljson.Marshal(topValue)
103107
}
104108

105109
// Generate generates the set of OpenAPI schema for all top-level types of the

encoding/openapi/orderedmap.go

+1-9
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,10 @@ package openapi
1616

1717
import (
1818
"cuelang.org/go/cue/ast"
19-
internaljson "cuelang.org/go/internal/encoding/json"
2019
)
2120

2221
// An OrderedMap is a set of key-value pairs that preserves the order in which
23-
// items were added. It marshals to JSON as an object.
22+
// items were added.
2423
//
2524
// Deprecated: the API now returns an ast.File. This allows OpenAPI to be
2625
// 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 {
9493
}
9594
return (*OrderedMap)(f.Value.(*ast.StructLit))
9695
}
97-
98-
// MarshalJSON implements [encoding/json.Marshaler].
99-
func (m *OrderedMap) MarshalJSON() (b []byte, err error) {
100-
// This is a pointer receiever to enforce that we only store pointers to
101-
// OrderedMap in the output.
102-
return internaljson.Encode((*ast.StructLit)(m))
103-
}

internal/encoding/json/encode.go

-288
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,6 @@ package json
1717
import (
1818
"bytes"
1919
"encoding/json"
20-
"math/big"
21-
"strings"
22-
23-
"cuelang.org/go/cue/ast"
24-
"cuelang.org/go/cue/errors"
25-
"cuelang.org/go/cue/literal"
26-
"cuelang.org/go/cue/token"
27-
"cuelang.org/go/internal"
28-
"cuelang.org/go/internal/astinternal"
2920
)
3021

3122
// Marshal is a replacement for [json.Marshal] without HTML escaping.
@@ -41,282 +32,3 @@ func Marshal(v any) ([]byte, error) {
4132
p = bytes.TrimSuffix(p, []byte("\n"))
4233
return p, nil
4334
}
44-
45-
// Encode converts a CUE AST to unescaped JSON.
46-
//
47-
// The given file must only contain values that can be directly supported by
48-
// JSON:
49-
//
50-
// Type Restrictions
51-
// BasicLit
52-
// File no imports, aliases, or definitions
53-
// StructLit no embeddings, aliases, or definitions
54-
// List
55-
// Field must be regular; label must be a BasicLit or Ident
56-
//
57-
// Comments and attributes are ignored.
58-
func Encode(n ast.Node) (b []byte, err error) {
59-
e := encoder{}
60-
err = e.encode(n)
61-
if err != nil {
62-
return nil, err
63-
}
64-
return e.w.Bytes(), nil
65-
}
66-
67-
type encoder struct {
68-
w bytes.Buffer
69-
tab []byte
70-
indentsAtLevel []int
71-
indenting bool
72-
unIndenting int
73-
}
74-
75-
func (e *encoder) writeIndent(b byte) {
76-
if e.indenting {
77-
e.indentsAtLevel[len(e.indentsAtLevel)-1]++
78-
} else {
79-
e.indentsAtLevel = append(e.indentsAtLevel, 0)
80-
}
81-
e.indenting = true
82-
_ = e.w.WriteByte(b)
83-
}
84-
85-
func (e *encoder) writeUnindent(b byte, pos, def token.Pos) {
86-
if e.unIndenting > 0 {
87-
e.unIndenting--
88-
} else {
89-
e.unIndenting = e.indentsAtLevel[len(e.indentsAtLevel)-1]
90-
e.indentsAtLevel = e.indentsAtLevel[:len(e.indentsAtLevel)-1]
91-
}
92-
e.indenting = false
93-
e.ws(pos, def.RelPos())
94-
_ = e.w.WriteByte(b)
95-
}
96-
97-
func (e *encoder) writeString(s string) {
98-
_, _ = e.w.WriteString(s)
99-
e.indenting = false
100-
}
101-
102-
func (e *encoder) writeByte(b byte) {
103-
_ = e.w.WriteByte(b)
104-
}
105-
106-
func (e *encoder) write(b []byte) {
107-
_, _ = e.w.Write(b)
108-
e.indenting = false
109-
}
110-
111-
func (e *encoder) indent() {
112-
for range e.indentsAtLevel {
113-
e.write(e.tab)
114-
}
115-
}
116-
117-
func (e *encoder) ws(pos token.Pos, default_ token.RelPos) {
118-
rel := pos.RelPos()
119-
if pos == token.NoPos {
120-
rel = default_
121-
}
122-
switch rel {
123-
case token.NoSpace:
124-
case token.Blank:
125-
e.writeByte(' ')
126-
case token.Newline:
127-
e.writeByte('\n')
128-
e.indent()
129-
case token.NewSection:
130-
e.writeString("\n\n")
131-
e.indent()
132-
}
133-
}
134-
func (e *encoder) encode(n ast.Node) error {
135-
if e.tab == nil {
136-
e.tab = []byte(" ")
137-
}
138-
const defPos = token.NoSpace
139-
switch x := n.(type) {
140-
case *ast.BasicLit:
141-
e.ws(x.Pos(), defPos)
142-
return e.encodeScalar(x, true)
143-
144-
case *ast.ListLit:
145-
e.ws(foldNewline(x.Pos()), token.NoRelPos)
146-
if len(x.Elts) == 0 {
147-
e.writeString("[]")
148-
return nil
149-
}
150-
e.writeIndent('[')
151-
for i, x := range x.Elts {
152-
if i > 0 {
153-
e.writeString(",")
154-
}
155-
if err := e.encode(x); err != nil {
156-
return err
157-
}
158-
}
159-
e.writeUnindent(']', x.Rbrack, compactNewline(x.Elts[0].Pos()))
160-
return nil
161-
162-
case *ast.StructLit:
163-
e.ws(foldNewline(n.Pos()), token.NoRelPos)
164-
return e.encodeDecls(x.Elts, x.Rbrace)
165-
166-
case *ast.File:
167-
return e.encodeDecls(x.Decls, token.NoPos)
168-
169-
case *ast.UnaryExpr:
170-
e.ws(foldNewline(x.Pos()), defPos)
171-
l, ok := x.X.(*ast.BasicLit)
172-
if ok && x.Op == token.SUB && (l.Kind == token.INT || l.Kind == token.FLOAT) {
173-
e.writeByte('-')
174-
return e.encodeScalar(l, false)
175-
}
176-
}
177-
return errors.Newf(n.Pos(), "json: unsupported node %s (%T)", astinternal.DebugStr(n), n)
178-
}
179-
180-
func (e *encoder) encodeScalar(l *ast.BasicLit, allowMinus bool) error {
181-
switch l.Kind {
182-
case token.INT:
183-
var x big.Int
184-
return e.setNum(l, allowMinus, &x)
185-
186-
case token.FLOAT:
187-
var x big.Float
188-
return e.setNum(l, allowMinus, &x)
189-
190-
case token.TRUE:
191-
e.writeString("true")
192-
193-
case token.FALSE:
194-
e.writeString("false")
195-
196-
case token.NULL:
197-
e.writeString("null")
198-
199-
case token.STRING:
200-
str, err := literal.Unquote(l.Value)
201-
if err != nil {
202-
return err
203-
}
204-
b, err := Marshal(str)
205-
if err != nil {
206-
return err
207-
}
208-
e.write(b)
209-
210-
default:
211-
return errors.Newf(l.Pos(), "unknown literal type %v", l.Kind)
212-
}
213-
return nil
214-
}
215-
216-
func (e *encoder) setNum(l *ast.BasicLit, allowMinus bool, x interface{}) error {
217-
if !allowMinus && strings.HasPrefix(l.Value, "-") {
218-
return errors.Newf(l.Pos(), "double minus not allowed")
219-
}
220-
var ni literal.NumInfo
221-
if err := literal.ParseNum(l.Value, &ni); err != nil {
222-
return err
223-
}
224-
e.writeString(ni.String())
225-
return nil
226-
}
227-
228-
// encodeDecls converts a sequence of declarations to a value. If it encounters
229-
// an embedded value, it will return this expression. This is more relaxed for
230-
// structs than is currently allowed for CUE, but the expectation is that this
231-
// will be allowed at some point. The input would still be illegal CUE.
232-
func (e *encoder) encodeDecls(decls []ast.Decl, endPos token.Pos) error {
233-
var embed ast.Expr
234-
var fields []*ast.Field
235-
236-
for _, d := range decls {
237-
switch x := d.(type) {
238-
default:
239-
return errors.Newf(x.Pos(), "json: unsupported node %s (%T)", astinternal.DebugStr(x), x)
240-
241-
case *ast.Package:
242-
if embed != nil || fields != nil {
243-
return errors.Newf(x.Pos(), "invalid package clause")
244-
}
245-
continue
246-
247-
case *ast.Field:
248-
if !internal.IsRegularField(x) {
249-
return errors.Newf(x.TokenPos, "json: definition or hidden field not allowed")
250-
}
251-
if x.Optional != token.NoPos {
252-
return errors.Newf(x.Optional, "json: optional fields not allowed")
253-
}
254-
fields = append(fields, x)
255-
256-
case *ast.EmbedDecl:
257-
if embed != nil {
258-
return errors.Newf(x.Pos(), "json: multiple embedded values")
259-
}
260-
embed = x.Expr
261-
262-
case *ast.CommentGroup:
263-
}
264-
}
265-
266-
if embed != nil {
267-
if fields != nil {
268-
return errors.Newf(embed.Pos(), "json: embedding mixed with fields")
269-
}
270-
return e.encode(embed)
271-
}
272-
273-
if len(fields) == 0 {
274-
e.writeString("{}")
275-
return nil
276-
}
277-
278-
e.writeIndent('{')
279-
pos := compactNewline(fields[0].Pos())
280-
if endPos == token.NoPos && pos.RelPos() == token.Blank {
281-
pos = token.NoPos
282-
}
283-
firstPos := pos
284-
const defPos = token.NoRelPos
285-
for i, x := range fields {
286-
if i > 0 {
287-
e.writeByte(',')
288-
pos = x.Pos()
289-
}
290-
name, _, err := ast.LabelName(x.Label)
291-
if err != nil {
292-
return errors.Newf(x.Label.Pos(), "json: only literal labels allowed")
293-
}
294-
b, err := Marshal(name)
295-
if err != nil {
296-
return err
297-
}
298-
e.ws(pos, defPos)
299-
e.write(b)
300-
e.writeByte(':')
301-
302-
if err := e.encode(x.Value); err != nil {
303-
return err
304-
}
305-
}
306-
e.writeUnindent('}', endPos, firstPos)
307-
return nil
308-
}
309-
310-
func compactNewline(pos token.Pos) token.Pos {
311-
if pos.RelPos() == token.NewSection {
312-
pos = token.Newline.Pos()
313-
}
314-
return pos
315-
}
316-
317-
func foldNewline(pos token.Pos) token.Pos {
318-
if pos.RelPos() >= token.Newline {
319-
pos = token.Blank.Pos()
320-
}
321-
return pos
322-
}

0 commit comments

Comments
 (0)