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