Skip to content

Commit

Permalink
Merge pull request #2 from adhocore/1-json5
Browse files Browse the repository at this point in the history
Implement most json5 spec
  • Loading branch information
adhocore authored Nov 19, 2022
2 parents 137689a + 5d4f05f commit 810fe03
Show file tree
Hide file tree
Showing 9 changed files with 3,573 additions and 95 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
.idea/
.DS_Store
*~
*.cached.json
*.exe
*.out
*.prof
vendor/
dist/
.env
106 changes: 100 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,74 @@
[![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Lightweight+fast+and+deps+free+commented+json+parser+for+Golang&url=https://github.com/adhocore/jsonc&hashtags=go,golang,parser,json,json-comment)


- Lightweight JSON comment stripper library for Go.
- Lightweight [JSON5](https://json5.org) pre-processor library for Go.
- Parses JSON5 input to JSON that Go understands. (Think of it as a superset to JSON.)
- Makes possible to have comment in any form of JSON data.
- Supported comments: single line `// comment` or multi line `/* comment */`.
- Also strips trailing comma at the end of array or object, eg:
- Supports trailing comma at the end of array or object, eg:
- `[1,2,,]` => `[1,2]`
- `{"x":1,,}` => `{"x":1}`
- Handles literal LF (newline/linefeed) within string notation so that we can have multiline string
- Supports JSON string inside JSON string
- Supports single quoted string.
- Supports object keys without quotes.
- Handles literal LF (linefeed) in string for splitting long lines.
- Supports explicit positive and hex number. `{"change": +10, "hex": 0xffff}`
- Supports decimal numbers with leading or trailing period. `{"leading": .5, "trailing": 2.}`
- Supports JSON string inside JSON string.
- Zero dependency (no vendor bloat).

---
### Example

This is [example](./examples/test.json5) of the JSON that you can parse with `adhocore/jsonc`:

```json5
/*start*/
//..
{
// this is line comment
a: [ // unquoted key
'bb', // single quoted string
"cc", // double quoted string
/* multi line
* comment
*/
123, // number
+10, // +ve number, equivalent to 10
-20, // -ve number
.25, // floating number, equivalent to 0.25
5., // floating number, equivalent to 5.0
0xabcDEF, // hex base16 number, equivalent to base10 counterpart: 11259375
{
123: 0xf, // even number as key?
xx: [1, .1, 'xyz',], y: '2', // array inside object, inside array
},
"// not a comment",
"/* also not a comment */",
['', "", true, false, null, 1, .5, 2., 0xf, // all sort of data types
{key:'val'/*comment*/,}], // object inside array, inside array
'single quoted',
],
/*aa*/aa: ['AA', {in: ['a', "b", ],},],
'd': { // single quoted key
t: /*creepy comment*/true, 'f': false,
a_b: 1, _1_z: 2, Ḁẟḕỻ: 'ɷɻɐỨẞṏḉ', // weird keys?
"n": null /*multiple trailing commas?*/,,,
/* 1 */
/* 2 */
},
"e": 'it\'s "good", eh?', // double quoted key, single quoted value with escaped quote
// json with comment inside json with comment, read that again:
"g": "/*comment*/{\"i\" : 1, \"url\" : \"http://foo.bar\" }//comment",
"h": "a new line after word 'literal'
this text is in a new line as there is literal EOL just above. \
but this one is continued in same line due to backslash escape",
// 1.
// 2.
}
//..
/*end*/
```

Find jsonc in [pkg.go.dev](https://pkg.go.dev/github.com/adhocore/jsonc).

## Installation
Expand All @@ -26,13 +84,19 @@ Find jsonc in [pkg.go.dev](https://pkg.go.dev/github.com/adhocore/jsonc).
go get -u github.com/adhocore/jsonc
```

## Usecase

You would ideally use this for organizing JSON configurations for humans to read and manage.
The JSON5 input is processed down into JSON which can be Unmarshal'ed by `encoding/json`.

For performance reasons you may also use [cached decoder](#cached-decoder) to have a cached copy of processed JSON output.

## Usage

Import and init library:
```go
import (
"fmt"

"github.com/adhocore/jsonc"
)

Expand Down Expand Up @@ -84,12 +148,42 @@ j.UnmarshalFile("./examples/test.json5", &out)
fmt.Printf("%+v\n", out)
```

### Cached Decoder

If you are weary of parsing same JSON5 source file over and over again, you can use cached decoder.
The source file is preprocessed and cached into output file with extension `.cached.json`.
It syncs the file `mtime` (aka modified time) from JSON5 source file to the cached JSON file to detect change.

The output file can then be consumed readily by `encoding/json`.
Leave that cached output untouched for machine and deal with source file only.
> (You can add `*.cached.json` to `.gitignore` if you so wish.)
As an example [examples/test.json5](./examples/test.json5) will be processed and cached into `examples/test.cached.json`.

Every change in source file `examples/test.json5` is reflected to the cached output on next call to `Decode()`
thus always maintaining the sync.

```go
import (
"fmt"
"github.com/adhocore/jsonc"
)

var dest map[string]interface{}
err := jsonc.NewCachedDecoder().Decode("./examples/test.json5", &dest);
if err != nil {
fmt.Printf("%+v", err)
} else {
fmt.Printf("%+v", dest)
}
```

> Run working [examples](./examples/main.go) with `go run examples/main.go`.
---
## License

> © [MIT](./LICENSE) | 2021-2099, Jitendra Adhikari
> © [MIT](./LICENSE) | 2022-2099, Jitendra Adhikari
---
### Other projects
Expand Down
54 changes: 54 additions & 0 deletions decode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package jsonc

import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"strings"
)

// CachedDecoder is a managed decoder that caches a copy of json5 transitioned to json
type CachedDecoder struct {
jsonc *Jsonc
ext string
}

// NewCachedDecoder gives a cached decoder
func NewCachedDecoder(ext ...string) *CachedDecoder {
ext = append(ext, ".cached.json")
return &CachedDecoder{New(), ext[0]}
}

// Decode decodes from cache if exists and relevant else decodes from source
func (fd *CachedDecoder) Decode(file string, v interface{}) error {
stat, err := os.Stat(file)
if err != nil {
return err
}

cache := strings.TrimSuffix(file, filepath.Ext(file)) + fd.ext
cstat, err := os.Stat(cache)
exist := !os.IsNotExist(err)
if err != nil && exist {
return err
}

// Update if not exist, or source file modified
update := !exist || stat.ModTime() != cstat.ModTime()
if !update {
jsonb, _ := ioutil.ReadFile(cache)
return json.Unmarshal(jsonb, v)
}

jsonb, _ := ioutil.ReadFile(file)
cfile, err := os.Create(cache)
if err != nil {
return err
}

jsonb = fd.jsonc.Strip(jsonb)
cfile.Write(jsonb)
os.Chtimes(cache, stat.ModTime(), stat.ModTime())
return json.Unmarshal(jsonb, v)
}
Loading

0 comments on commit 810fe03

Please sign in to comment.