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 json converts JSON to CUE.
16// To convert CUE to JSON, use [encoding/json.Marshal] on a [cue.Value].
17package json
18
19import (
20 "bytes"
21 "encoding/json"
22 "fmt"
23 "io"
24
25 "cuelang.org/go/cue"
26 "cuelang.org/go/cue/ast"
27 "cuelang.org/go/cue/ast/astutil"
28 "cuelang.org/go/cue/errors"
29 "cuelang.org/go/cue/literal"
30 "cuelang.org/go/cue/parser"
31 "cuelang.org/go/cue/token"
32 "cuelang.org/go/internal/source"
33)
34
35// Valid reports whether data is a valid JSON encoding.
36func Valid(b []byte) bool {
37 return json.Valid(b)
38}
39
40// Validate validates JSON and confirms it matches the constraints
41// specified by v.
42func Validate(b []byte, v cue.Value) error {
43 if !json.Valid(b) {
44 return fmt.Errorf("json: invalid JSON")
45 }
46 v2 := v.Context().CompileBytes(b, cue.Filename("json.Validate"))
47 if err := v2.Err(); err != nil {
48 return err
49 }
50
51 v = v.Unify(v2)
52 if err := v.Err(); err != nil {
53 return err
54 }
55 return v.Validate(cue.Final())
56}
57
58// Extract parses JSON-encoded data to a CUE expression, using path for
59// position information.
60func Extract(path string, data []byte) (ast.Expr, error) {
61 expr, err := extract(path, data)
62 if err != nil {
63 return nil, err
64 }
65 patchExpr(expr, nil)
66 return expr, nil
67}
68
69func extract(path string, b []byte) (ast.Expr, error) {
70 expr, err := parser.ParseExpr(path, b)
71 if err != nil || !json.Valid(b) {
72 p := token.NoPos
73 if pos := errors.Positions(err); len(pos) > 0 {
74 p = pos[0]
75 }
76 var x interface{}
77 err := json.Unmarshal(b, &x)
78
79 // If encoding/json has a position, prefer that, as it relates to json.Unmarshal's error message.
80 if synErr, ok := err.(*json.SyntaxError); ok && len(b) > 0 {
81 tokFile := token.NewFile(path, 0, len(b))
82 tokFile.SetLinesForContent(b)
83 p = tokFile.Pos(int(synErr.Offset-1), token.NoRelPos)
84 }
85
86 return nil, errors.Wrapf(err, p, "invalid JSON for file %q", path)
87 }
88 return expr, nil
89}
90
91// NewDecoder configures a JSON decoder. The path is used to associate position
92// information with each node. The runtime may be nil if the decoder
93// is only used to extract to CUE ast objects.
94//
95// The runtime argument is a historical remnant and unused.
96func NewDecoder(r *cue.Runtime, path string, src io.Reader) *Decoder {
97 b, err := source.ReadAll(path, src)
98 tokFile := token.NewFile(path, 0, len(b))
99 tokFile.SetLinesForContent(b)
100 return &Decoder{
101 path: path,
102 dec: json.NewDecoder(bytes.NewReader(b)),
103 tokFile: tokFile,
104 readAllErr: err,
105 }
106}
107
108// A Decoder converts JSON values to CUE.
109type Decoder struct {
110 path string
111 dec *json.Decoder
112
113 startOffset int
114 tokFile *token.File
115 readAllErr error
116}
117
118// Extract converts the current JSON value to a CUE ast. It returns io.EOF
119// if the input has been exhausted.
120func (d *Decoder) Extract() (ast.Expr, error) {
121 if d.readAllErr != nil {
122 return nil, d.readAllErr
123 }
124
125 expr, err := d.extract()
126 if err != nil {
127 return expr, err
128 }
129 patchExpr(expr, d.patchPos)
130 return expr, nil
131}
132
133func (d *Decoder) extract() (ast.Expr, error) {
134 var raw json.RawMessage
135 err := d.dec.Decode(&raw)
136 if err == io.EOF {
137 return nil, err
138 }
139 if err != nil {
140 pos := token.NoPos
141 // When decoding into a RawMessage, encoding/json should only error due to syntax errors.
142 if synErr, ok := err.(*json.SyntaxError); ok {
143 pos = d.tokFile.Pos(int(synErr.Offset-1), token.NoRelPos)
144 }
145 return nil, errors.Wrapf(err, pos, "invalid JSON for file %q", d.path)
146 }
147 expr, err := parser.ParseExpr(d.path, []byte(raw))
148 if err != nil {
149 return nil, err
150 }
151
152 d.startOffset = int(d.dec.InputOffset()) - len(raw)
153 return expr, nil
154}
155
156func (d *Decoder) patchPos(n ast.Node) {
157 pos := n.Pos()
158 realPos := d.tokFile.Pos(pos.Offset()+d.startOffset, pos.RelPos())
159 ast.SetPos(n, realPos)
160}
161
162// patchExpr simplifies the AST parsed from JSON.
163// TODO: some of the modifications are already done in format, but are
164// a package deal of a more aggressive simplify. Other pieces of modification
165// should probably be moved to format.
166func patchExpr(n ast.Node, patchPos func(n ast.Node)) {
167 type info struct {
168 reflow bool
169 }
170 stack := []info{{true}}
171
172 afterFn := func(n ast.Node) {
173 switch n.(type) {
174 case *ast.ListLit, *ast.StructLit:
175 stack = stack[:len(stack)-1]
176 }
177 }
178
179 var beforeFn func(n ast.Node) bool
180
181 beforeFn = func(n ast.Node) bool {
182 if patchPos != nil {
183 patchPos(n)
184 }
185
186 isLarge := n.End().Offset()-n.Pos().Offset() > 50
187 descent := true
188
189 switch x := n.(type) {
190 case *ast.ListLit:
191 reflow := true
192 if !isLarge {
193 for _, e := range x.Elts {
194 if hasSpaces(e) {
195 reflow = false
196 break
197 }
198 }
199 }
200 stack = append(stack, info{reflow})
201 if reflow {
202 x.Lbrack = x.Lbrack.WithRel(token.NoRelPos)
203 x.Rbrack = x.Rbrack.WithRel(token.NoRelPos)
204 }
205 return true
206
207 case *ast.StructLit:
208 reflow := true
209 if !isLarge {
210 for _, e := range x.Elts {
211 if f, ok := e.(*ast.Field); !ok || hasSpaces(f) || hasSpaces(f.Value) {
212 reflow = false
213 break
214 }
215 }
216 }
217 stack = append(stack, info{reflow})
218 if reflow {
219 x.Lbrace = x.Lbrace.WithRel(token.NoRelPos)
220 x.Rbrace = x.Rbrace.WithRel(token.NoRelPos)
221 }
222 return true
223
224 case *ast.Field:
225 // label is always a string for JSON.
226 s, ok := x.Label.(*ast.BasicLit)
227 if !ok || s.Kind != token.STRING {
228 break // should not happen: implies invalid JSON
229 }
230
231 u, err := literal.Unquote(s.Value)
232 if err != nil {
233 break // should not happen: implies invalid JSON
234 }
235
236 // TODO(legacy): remove checking for '_' prefix once hidden
237 // fields are removed.
238 if ast.StringLabelNeedsQuoting(u) {
239 break // keep string
240 }
241
242 x.Label = ast.NewIdent(u)
243 astutil.CopyMeta(x.Label, s)
244 // Having removed the quote-marks, the ident start should be
245 // incremented by 1 so that the label content matches up with
246 // the raw json.
247 ast.SetPos(x.Label, x.Label.Pos().Add(1))
248
249 ast.Walk(x.Value, beforeFn, afterFn)
250 descent = false
251
252 case *ast.BasicLit:
253 if x.Kind == token.STRING && len(x.Value) > 10 {
254 s, err := literal.Unquote(x.Value)
255 if err != nil {
256 break // should not happen: implies invalid JSON
257 }
258
259 x.Value = literal.String.WithOptionalTabIndent(len(stack)).Quote(s)
260 }
261 }
262
263 if stack[len(stack)-1].reflow {
264 ast.SetRelPos(n, token.NoRelPos)
265 }
266 return descent
267 }
268
269 ast.Walk(n, beforeFn, afterFn)
270}
271
272func hasSpaces(n ast.Node) bool {
273 return n.Pos().RelPos() > token.NoSpace
274}