Skip to content

Single quoted strings #39

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,29 @@ func ExampleGet_filter() {
// II
}

func ExampleGet_filterQuoted() {
v := interface{}(nil)

json.Unmarshal([]byte(`[
{"key":"alpha","value" : "I"},
{"key":"beta","value" : "II"},
{"key":"gamma","value" : "III"}
]`), &v)

values, err := jsonpath.Get(`$[? @.key=='beta'].value`, v)
if err != nil {
fmt.Println(err)
os.Exit(1)
}

for _, value := range values.([]interface{}) {
fmt.Println(value)
}

// Output:
// II
}

func Example_gval() {
builder := gval.Full(jsonpath.PlaceholderExtension())

Expand Down
28 changes: 18 additions & 10 deletions jsonpath.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,30 @@ func Get(path string, value interface{}) (interface{}, error) {
return eval(context.Background(), value)
}

var lang = gval.NewLanguage(
gval.Base(),
gval.PrefixExtension('$', parseRootPath),
gval.PrefixExtension('@', parseCurrentPath),
)
var lang = func() gval.Language {
l := gval.NewLanguage(
gval.Base(),
gval.PrefixExtension('$', parseRootPath),
gval.PrefixExtension('@', parseCurrentPath),
)
l.CreateScanner(CreateScanner)
return l
}()

// Language is the JSONPath Language
func Language() gval.Language {
return lang
}

var placeholderExtension = gval.NewLanguage(
lang,
gval.PrefixExtension('{', parseJSONObject),
gval.PrefixExtension('#', parsePlaceholder),
)
var placeholderExtension = func() gval.Language {
l := gval.NewLanguage(
lang,
gval.PrefixExtension('{', parseJSONObject),
gval.PrefixExtension('#', parsePlaceholder),
)
l.CreateScanner(CreateScanner)
return l
}()

// PlaceholderExtension is the JSONPath Language with placeholder
func PlaceholderExtension() gval.Language {
Expand Down
10 changes: 5 additions & 5 deletions parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"context"
"fmt"
"math"
"text/scanner"
goscanner "text/scanner"

"github.com/PaesslerAG/gval"
)
Expand Down Expand Up @@ -81,7 +81,7 @@ func (p *parser) parsePath(c context.Context) error {
func (p *parser) parseSelect(c context.Context) error {
scan := p.Scan()
switch scan {
case scanner.Ident:
case goscanner.Ident:
p.appendPlainSelector(directSelector(p.Const(p.TokenText())))
return p.parsePath(c)
case '.':
Expand All @@ -91,7 +91,7 @@ func (p *parser) parseSelect(c context.Context) error {
p.appendAmbiguousSelector(starSelector())
return p.parsePath(c)
default:
return p.Expected("JSON select", scanner.Ident, '.', '*')
return p.Expected("JSON select", goscanner.Ident, '.', '*')
}
}

Expand Down Expand Up @@ -154,7 +154,7 @@ func (p *parser) parseBracket(c context.Context) (keys []gval.Evaluable, seperat
func (p *parser) parseMapper(c context.Context) error {
scan := p.Scan()
switch scan {
case scanner.Ident:
case goscanner.Ident:
p.appendPlainSelector(directSelector(p.Const(p.TokenText())))
case '[':
keys, seperator, err := p.parseBracket(c)
Expand All @@ -178,7 +178,7 @@ func (p *parser) parseMapper(c context.Context) error {
case '(':
return p.parseScript(c)
default:
return p.Expected("JSON mapper", '[', scanner.Ident, '*')
return p.Expected("JSON mapper", '[', goscanner.Ident, '*')
}
return p.parsePath(c)
}
Expand Down
4 changes: 2 additions & 2 deletions placeholder.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"context"
"fmt"
"strconv"
"text/scanner"
goscanner "text/scanner"

"github.com/PaesslerAG/gval"
)
Expand Down Expand Up @@ -134,7 +134,7 @@ func parsePlaceholder(c context.Context, p *gval.Parser) (gval.Evaluable, error)
}
*(hasWildcard.(*bool)) = true
switch p.Scan() {
case scanner.Int:
case goscanner.Int:
id, err := strconv.Atoi(p.TokenText())
if err != nil {
return nil, err
Expand Down
117 changes: 117 additions & 0 deletions quote.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package jsonpath

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This is mainly taken from
//
// https://cs.opensource.google/go/go/+/refs/tags/go1.21.0:src/strconv/quote.go
//
// and adjusted to meet the needs of unquoting JSONPath strings.
// Mainly handling single quoted strings right and removed support for
// Go raw strings.

import (
"strconv"
"strings"
"unicode/utf8"
)

// contains reports whether the string contains the byte c.
func contains(s string, c byte) bool {
return strings.IndexByte(s, c) != -1
}

// unquote interprets s as a single-quoted, double-quoted,
// or backquoted Go string literal, returning the string value
// that s quotes. (If s is single-quoted, it would be a Go
// character literal; Unquote returns the corresponding
// one-character string.)
func unquote(s string) (string, error) {
out, rem, err := unquoteInternal(s)
if len(rem) > 0 {
return "", strconv.ErrSyntax
}
return out, err
}

// unquote parses a quoted string at the start of the input,
// returning the parsed prefix, the remaining suffix, and any parse errors.
// If unescape is true, the parsed prefix is unescaped,
// otherwise the input prefix is provided verbatim.
func unquoteInternal(in string) (out, rem string, err error) {
// In our use case it's a constant.
const unescape = true
// Determine the quote form and optimistically find the terminating quote.
if len(in) < 2 {
return "", in, strconv.ErrSyntax
}
quote := in[0]
end := strings.IndexByte(in[1:], quote)
if end < 0 {
return "", in, strconv.ErrSyntax
}
end += 2 // position after terminating quote; may be wrong if escape sequences are present

switch quote {
case '"', '\'':
// Handle quoted strings without any escape sequences.
if !contains(in[:end], '\\') && !contains(in[:end], '\n') {
var ofs int
if quote == '\'' {
ofs = len(`"`)
} else {
ofs = len(`'`)
}
valid := utf8.ValidString(in[ofs : end-ofs])
if valid {
out = in[:end]
if unescape {
out = out[1 : end-1] // exclude quotes
}
return out, in[end:], nil
}
}

// Handle quoted strings with escape sequences.
var buf []byte
in0 := in
in = in[1:] // skip starting quote
if unescape {
buf = make([]byte, 0, 3*end/2) // try to avoid more allocations
}
for len(in) > 0 && in[0] != quote {
// Process the next character,
// rejecting any unescaped newline characters which are invalid.
r, multibyte, rem, err := strconv.UnquoteChar(in, quote)
if in[0] == '\n' || err != nil {
return "", in0, strconv.ErrSyntax
}
in = rem

// Append the character if unescaping the input.
if unescape {
if r < utf8.RuneSelf || !multibyte {
buf = append(buf, byte(r))
} else {
var arr [utf8.UTFMax]byte
n := utf8.EncodeRune(arr[:], r)
buf = append(buf, arr[:n]...)
}
}
}

// Verify that the string ends with a terminating quote.
if !(len(in) > 0 && in[0] == quote) {
return "", in0, strconv.ErrSyntax
}
in = in[1:] // skip terminating quote

if unescape {
return string(buf), in, nil
}
return in0[:len(in0)-len(in)], in, nil
default:
return "", in, strconv.ErrSyntax
}
}
Loading