json: Parsing of strings and objects

This commit is contained in:
Martin Atkins 2017-05-16 08:00:07 -07:00
parent b5a78fd826
commit 4100bdfd2f
2 changed files with 280 additions and 15 deletions

View File

@ -1,6 +1,7 @@
package json package json
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/apparentlymart/go-zcl/zcl" "github.com/apparentlymart/go-zcl/zcl"
@ -16,7 +17,12 @@ func parseFileContent(buf []byte, filename string) (node, zcl.Diagnostics) {
}, },
}) })
p := newPeeker(tokens) p := newPeeker(tokens)
return parseValue(p) node, diags := parseValue(p)
if diags.HasErrors() {
// Don't return a node if there were errors during parsing.
return nil, diags
}
return node, diags
} }
func parseValue(p *peeker) (node, zcl.Diagnostics) { func parseValue(p *peeker) (node, zcl.Diagnostics) {
@ -103,7 +109,7 @@ Token:
key := keyStrNode.Value key := keyStrNode.Value
colon := p.Read() colon := p.Read()
if colon.Type != tokenComma { if colon.Type != tokenColon {
if colon.Type == tokenBraceC || colon.Type == tokenComma { if colon.Type == tokenBraceC || colon.Type == tokenComma {
// Catch common mistake of using braces instead of brackets // Catch common mistake of using braces instead of brackets
// for an array. // for an array.
@ -129,6 +135,19 @@ Token:
return nil, diags return nil, diags
} }
if existing := attrs[key]; existing != nil {
// Generate a diagnostic for the duplicate key, but continue parsing
// anyway since this is a semantic error we can recover from.
diags = diags.Append(&zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Duplicate object attribute",
Detail: fmt.Sprintf(
"An attribute named %q was previously introduced at %s",
key, existing.NameRange.String(),
),
Subject: &colon.Range,
})
}
attrs[key] = &objectAttr{ attrs[key] = &objectAttr{
Name: key, Name: key,
Value: valNode, Value: valNode,
@ -148,6 +167,20 @@ Token:
}) })
} }
continue Token continue Token
case tokenEOF:
return nil, diags.Append(&zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Unclosed object",
Detail: "No closing brace was found for this JSON object.",
Subject: &open.Range,
})
case tokenBrackC:
return nil, diags.Append(&zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Mismatched braces",
Detail: "A JSON object must be closed with a brace, not a bracket.",
Subject: p.Peek().Range.Ptr(),
})
case tokenBraceC: case tokenBraceC:
break Token break Token
default: default:
@ -178,7 +211,58 @@ func parseNumber(p *peeker) (node, zcl.Diagnostics) {
} }
func parseString(p *peeker) (node, zcl.Diagnostics) { func parseString(p *peeker) (node, zcl.Diagnostics) {
return nil, nil tok := p.Read()
var str string
err := json.Unmarshal(tok.Bytes, &str)
if err != nil {
var errRange zcl.Range
if serr, ok := err.(*json.SyntaxError); ok {
errOfs := serr.Offset
errPos := tok.Range.Start
errPos.Byte += int(errOfs)
// TODO: Use the byte offset to properly count unicode
// characters for the column, and mark the whole of the
// character that was wrong as part of our range.
errPos.Column += int(errOfs)
errEndPos := errPos
errEndPos.Byte++
errEndPos.Column++
errRange = zcl.Range{
Filename: tok.Range.Filename,
Start: errPos,
End: errEndPos,
}
} else {
errRange = tok.Range
}
var contextRange *zcl.Range
if errRange != tok.Range {
contextRange = &tok.Range
}
// FIXME: Eventually we should parse strings directly here so
// we can produce a more useful error message in the face fo things
// such as invalid escapes, etc.
return nil, zcl.Diagnostics{
{
Severity: zcl.DiagError,
Summary: "Invalid JSON string",
Detail: fmt.Sprintf("There is a syntax error in the given JSON string."),
Subject: &errRange,
Context: contextRange,
},
}
}
return &stringVal{
Value: str,
SrcRange: tok.Range,
}, nil
} }
func parseKeyword(p *peeker) (node, zcl.Diagnostics) { func parseKeyword(p *peeker) (node, zcl.Diagnostics) {

View File

@ -14,14 +14,14 @@ func TestParse(t *testing.T) {
Want node Want node
DiagCount int DiagCount int
}{ }{
// Simple, single-token constructs
{ {
`true`, `true`,
&booleanVal{ &booleanVal{
Value: true, Value: true,
SrcRange: zcl.Range{ SrcRange: zcl.Range{
Filename: "", Start: zcl.Pos{Line: 1, Column: 1, Byte: 0},
Start: zcl.Pos{Line: 1, Column: 1, Byte: 0}, End: zcl.Pos{Line: 1, Column: 5, Byte: 4},
End: zcl.Pos{Line: 1, Column: 5, Byte: 4},
}, },
}, },
0, 0,
@ -31,9 +31,8 @@ func TestParse(t *testing.T) {
&booleanVal{ &booleanVal{
Value: false, Value: false,
SrcRange: zcl.Range{ SrcRange: zcl.Range{
Filename: "", Start: zcl.Pos{Line: 1, Column: 1, Byte: 0},
Start: zcl.Pos{Line: 1, Column: 1, Byte: 0}, End: zcl.Pos{Line: 1, Column: 6, Byte: 5},
End: zcl.Pos{Line: 1, Column: 6, Byte: 5},
}, },
}, },
0, 0,
@ -42,9 +41,8 @@ func TestParse(t *testing.T) {
`null`, `null`,
&nullVal{ &nullVal{
SrcRange: zcl.Range{ SrcRange: zcl.Range{
Filename: "", Start: zcl.Pos{Line: 1, Column: 1, Byte: 0},
Start: zcl.Pos{Line: 1, Column: 1, Byte: 0}, End: zcl.Pos{Line: 1, Column: 5, Byte: 4},
End: zcl.Pos{Line: 1, Column: 5, Byte: 4},
}, },
}, },
0, 0,
@ -59,6 +57,186 @@ func TestParse(t *testing.T) {
nil, nil,
1, 1,
}, },
{
`"hello"`,
&stringVal{
Value: "hello",
SrcRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 1, Byte: 0},
End: zcl.Pos{Line: 1, Column: 8, Byte: 7},
},
},
0,
},
{
`"hello\nworld"`,
&stringVal{
Value: "hello\nworld",
SrcRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 1, Byte: 0},
End: zcl.Pos{Line: 1, Column: 15, Byte: 14},
},
},
0,
},
{
`"hello \"world\""`,
&stringVal{
Value: `hello "world"`,
SrcRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 1, Byte: 0},
End: zcl.Pos{Line: 1, Column: 18, Byte: 17},
},
},
0,
},
{
`"hello \\"`,
&stringVal{
Value: "hello \\",
SrcRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 1, Byte: 0},
End: zcl.Pos{Line: 1, Column: 11, Byte: 10},
},
},
0,
},
{
`"hello`,
nil,
1,
},
{
`"he\llo"`,
nil,
1,
},
// Objects
{
`{"hello": true}`,
&objectVal{
Attrs: map[string]*objectAttr{
"hello": {
Name: "hello",
Value: &booleanVal{
Value: true,
SrcRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 11, Byte: 10},
End: zcl.Pos{Line: 1, Column: 15, Byte: 14},
},
},
NameRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 2, Byte: 1},
End: zcl.Pos{Line: 1, Column: 9, Byte: 8},
},
},
},
SrcRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 1, Byte: 0},
End: zcl.Pos{Line: 1, Column: 16, Byte: 15},
},
OpenRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 1, Byte: 0},
End: zcl.Pos{Line: 1, Column: 2, Byte: 1},
},
},
0,
},
{
`{"hello": true, "bye": false}`,
&objectVal{
Attrs: map[string]*objectAttr{
"hello": {
Name: "hello",
Value: &booleanVal{
Value: true,
SrcRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 11, Byte: 10},
End: zcl.Pos{Line: 1, Column: 15, Byte: 14},
},
},
NameRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 2, Byte: 1},
End: zcl.Pos{Line: 1, Column: 9, Byte: 8},
},
},
"bye": {
Name: "bye",
Value: &booleanVal{
Value: false,
SrcRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 24, Byte: 23},
End: zcl.Pos{Line: 1, Column: 29, Byte: 28},
},
},
NameRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 17, Byte: 16},
End: zcl.Pos{Line: 1, Column: 22, Byte: 21},
},
},
},
SrcRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 1, Byte: 0},
End: zcl.Pos{Line: 1, Column: 30, Byte: 29},
},
OpenRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 1, Byte: 0},
End: zcl.Pos{Line: 1, Column: 2, Byte: 1},
},
},
0,
},
{
`{}`,
&objectVal{
Attrs: map[string]*objectAttr{},
SrcRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 1, Byte: 0},
End: zcl.Pos{Line: 1, Column: 3, Byte: 2},
},
OpenRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 1, Byte: 0},
End: zcl.Pos{Line: 1, Column: 2, Byte: 1},
},
},
0,
},
{
`{"hello":true`,
nil,
1,
},
{
`{"hello":true]`,
nil,
1,
},
{
`{"hello":true,}`,
nil,
1,
},
{
`{true:false}`,
nil,
1,
},
{
`{"hello": true, "hello": true}`,
nil,
1,
},
{
`{"hello": true, "hello": true, "hello", true}`,
nil,
2,
},
{
`{"hello", "world"}`,
nil,
1,
},
} }
for _, test := range tests { for _, test := range tests {
@ -66,13 +244,16 @@ func TestParse(t *testing.T) {
got, diag := parseFileContent([]byte(test.Input), "") got, diag := parseFileContent([]byte(test.Input), "")
if len(diag) != test.DiagCount { if len(diag) != test.DiagCount {
t.Errorf("got %d diagnostics; want %d\n%s", len(diag), test.DiagCount, spew.Sdump(diag)) t.Errorf("got %d diagnostics; want %d", len(diag), test.DiagCount)
for _, d := range diag {
t.Logf(" - %s", d.Error())
}
} }
if !reflect.DeepEqual(got, test.Want) { if !reflect.DeepEqual(got, test.Want) {
t.Errorf( t.Errorf(
"wrong result\ninput: %s\ngot: %#v\nwant: %#v", "wrong result\ninput: %s\ngot: %s\nwant: %s",
test.Input, got, test.Want, test.Input, spew.Sdump(got), spew.Sdump(test.Want),
) )
} }
}) })