2023-03-01 02:19:06 +00:00
//go:build !yq_nojson
2022-01-21 09:26:09 +00:00
package yqlib
import (
"bufio"
"bytes"
"fmt"
"testing"
"github.com/mikefarah/yq/v4/test"
)
2023-10-18 01:11:53 +00:00
const complexExpectYaml = ` a : Easy ! as one two three
2022-01-21 09:26:09 +00:00
b :
2023-10-18 01:11:53 +00:00
c : 2
d :
- 3
- 4
2022-01-21 09:26:09 +00:00
`
2022-07-27 02:26:22 +00:00
const sampleNdJson = ` { "this" : "is a multidoc json file" }
{ "each" : [ "line is a valid json document" ] }
{ "a number" : 4 }
`
2022-08-01 06:26:43 +00:00
const sampleNdJsonKey = ` { "a": "first", "b": "next", "ab": "last"} `
const expectedJsonKeysInOrder = ` a : first
b : next
ab : last
`
2022-07-27 02:26:22 +00:00
const expectedNdJsonYaml = ` this : is a multidoc json file
-- -
each :
- line is a valid json document
-- -
a number : 4
`
const expectedRoundTripSampleNdJson = ` { "this" : "is a multidoc json file" }
{ "each" : [ "line is a valid json document" ] }
{ "a number" : 4 }
`
const expectedUpdatedMultilineJson = ` { "this" : "is a multidoc json file" }
{ "each" : [ "line is a valid json document" , "cool" ] }
{ "a number" : 4 }
`
const sampleMultiLineJson = ` {
"this" : "is a multidoc json file"
}
{
"it" : [
"has" ,
"consecutive" ,
"json documents"
]
}
{
"a number" : 4
}
`
const roundTripMultiLineJson = ` {
"this" : "is a multidoc json file"
}
{
"it" : [
"has" ,
"consecutive" ,
"json documents"
]
}
{
"a number" : 4
}
`
2022-01-21 09:26:09 +00:00
var jsonScenarios = [ ] formatScenario {
2023-11-23 00:53:18 +00:00
{
description : "array empty" ,
skipDoc : true ,
input : "[]" ,
scenarioType : "roundtrip-ndjson" ,
expected : "[]\n" ,
} ,
{
description : "array has scalar" ,
skipDoc : true ,
input : "[3]" ,
scenarioType : "roundtrip-ndjson" ,
expected : "[3]\n" ,
} ,
{
description : "array has object" ,
skipDoc : true ,
input : ` [ { "x": 3}] ` ,
scenarioType : "roundtrip-ndjson" ,
expected : "[{\"x\":3}]\n" ,
} ,
2023-05-04 04:48:57 +00:00
{
description : "array null" ,
skipDoc : true ,
input : "[null]" ,
scenarioType : "roundtrip-ndjson" ,
expected : "[null]\n" ,
} ,
2022-11-15 00:42:31 +00:00
{
description : "set tags" ,
skipDoc : true ,
input : "[{}]" ,
expression : ` [.. | type] ` ,
scenarioType : "roundtrip-ndjson" ,
expected : "[\"!!seq\",\"!!map\"]\n" ,
} ,
2022-01-21 09:26:09 +00:00
{
description : "Parse json: simple" ,
subdescription : "JSON is a subset of yaml, so all you need to do is prettify the output" ,
input : ` { "cat": "meow"} ` ,
2023-10-18 01:11:53 +00:00
scenarioType : "decode-ndjson" ,
expected : "cat: meow\n" ,
} ,
{
skipDoc : true ,
description : "Parse json: simple: key" ,
input : ` { "cat": "meow"} ` ,
expression : ".cat | key" ,
expected : "\"cat\"\n" ,
scenarioType : "decode" ,
} ,
{
skipDoc : true ,
description : "Parse json: simple: parent" ,
input : ` { "cat": "meow"} ` ,
expression : ".cat | parent" ,
expected : "{\"cat\":\"meow\"}\n" ,
scenarioType : "decode" ,
} ,
{
skipDoc : true ,
description : "Parse json: simple: path" ,
input : ` { "cat": "meow"} ` ,
expression : ".cat | path" ,
expected : "[\"cat\"]\n" ,
scenarioType : "decode" ,
} ,
{
skipDoc : true ,
description : "Parse json: deeper: path" ,
input : ` { "cat": { "noises": "meow"}} ` ,
expression : ".cat.noises | path" ,
expected : "[\"cat\",\"noises\"]\n" ,
scenarioType : "decode" ,
} ,
{
skipDoc : true ,
description : "Parse json: array path" ,
input : ` { "cat": { "noises": ["meow"]}} ` ,
expression : ".cat.noises[0] | path" ,
expected : "[\"cat\",\"noises\",0]\n" ,
scenarioType : "decode" ,
2022-01-21 09:26:09 +00:00
} ,
2022-11-10 11:33:38 +00:00
{
description : "bad json" ,
skipDoc : true ,
2023-10-18 01:11:53 +00:00
input : ` { "a": 1 b": 2} ` ,
expectedError : ` bad file 'sample.yml': json: string of object unexpected end of JSON input ` ,
2022-11-10 11:33:38 +00:00
scenarioType : "decode-error" ,
} ,
2022-01-21 09:26:09 +00:00
{
description : "Parse json: complex" ,
subdescription : "JSON is a subset of yaml, so all you need to do is prettify the output" ,
input : ` { "a":"Easy! as one two three","b": { "c":2,"d":[3,4]}} ` ,
expected : complexExpectYaml ,
2023-10-18 01:11:53 +00:00
scenarioType : "decode-ndjson" ,
2022-01-21 09:26:09 +00:00
} ,
{
description : "Encode json: simple" ,
input : ` cat: meow ` ,
indent : 2 ,
expected : "{\n \"cat\": \"meow\"\n}\n" ,
scenarioType : "encode" ,
} ,
{
description : "Encode json: simple - in one line" ,
input : ` cat: meow # this is a comment, and it will be dropped. ` ,
indent : 0 ,
expected : "{\"cat\":\"meow\"}\n" ,
scenarioType : "encode" ,
} ,
{
description : "Encode json: comments" ,
input : ` cat: meow # this is a comment, and it will be dropped. ` ,
indent : 2 ,
expected : "{\n \"cat\": \"meow\"\n}\n" ,
scenarioType : "encode" ,
} ,
{
description : "Encode json: anchors" ,
subdescription : "Anchors are dereferenced" ,
input : "cat: &ref meow\nanotherCat: *ref" ,
indent : 2 ,
expected : "{\n \"cat\": \"meow\",\n \"anotherCat\": \"meow\"\n}\n" ,
scenarioType : "encode" ,
} ,
{
description : "Encode json: multiple results" ,
subdescription : "Each matching node is converted into a json doc. This is best used with 0 indent (json document per line)" ,
input : ` things: [ { stuff: cool}, { whatever: cat}] ` ,
expression : ` .things[] ` ,
indent : 0 ,
expected : "{\"stuff\":\"cool\"}\n{\"whatever\":\"cat\"}\n" ,
scenarioType : "encode" ,
} ,
2022-07-27 02:26:22 +00:00
{
2023-12-01 00:39:02 +00:00
description : "Roundtrip JSON Lines / NDJSON" ,
input : sampleNdJson ,
expected : expectedRoundTripSampleNdJson ,
scenarioType : "roundtrip-ndjson" ,
2022-07-27 02:26:22 +00:00
} ,
{
description : "Roundtrip multi-document JSON" ,
2023-12-01 00:39:02 +00:00
subdescription : "The parser can also handle multiple multi-line json documents in a single file (despite this not being in the JSON Lines / NDJSON spec). Typically you would have one entire JSON document per line, but the parser also supports multiple multi-line json documents" ,
2022-07-27 02:26:22 +00:00
input : sampleMultiLineJson ,
expected : roundTripMultiLineJson ,
scenarioType : "roundtrip-multi" ,
} ,
{
description : "Update a specific document in a multi-document json" ,
subdescription : "Documents are indexed by the `documentIndex` or `di` operator." ,
input : sampleNdJson ,
expected : expectedUpdatedMultilineJson ,
expression : ` (select(di == 1) | .each ) += "cool" ` ,
scenarioType : "roundtrip-ndjson" ,
} ,
{
description : "Find and update a specific document in a multi-document json" ,
subdescription : "Use expressions as you normally would." ,
input : sampleNdJson ,
expected : expectedUpdatedMultilineJson ,
expression : ` (select(has("each")) | .each ) += "cool" ` ,
scenarioType : "roundtrip-ndjson" ,
} ,
{
2023-12-01 00:39:02 +00:00
description : "Decode JSON Lines / NDJSON" ,
2022-07-27 02:26:22 +00:00
input : sampleNdJson ,
expected : expectedNdJsonYaml ,
scenarioType : "decode-ndjson" ,
} ,
2022-08-01 06:26:43 +00:00
{
2023-12-01 00:39:02 +00:00
description : "Decode JSON Lines / NDJSON, maintain key order" ,
2022-08-01 06:26:43 +00:00
skipDoc : true ,
input : sampleNdJsonKey ,
expected : expectedJsonKeysInOrder ,
scenarioType : "decode-ndjson" ,
} ,
2022-07-27 02:26:22 +00:00
{
description : "numbers" ,
skipDoc : true ,
2023-09-18 23:42:07 +00:00
input : "[3, 3.0, 3.1, -1, 999999, 1000000, 1000001, 1.1]" ,
expected : "- 3\n- 3\n- 3.1\n- -1\n- 999999\n- 1000000\n- 1000001\n- 1.1\n" ,
2022-07-27 02:26:22 +00:00
scenarioType : "decode-ndjson" ,
} ,
2022-08-01 06:26:43 +00:00
{
description : "number single" ,
skipDoc : true ,
input : "3" ,
expected : "3\n" ,
scenarioType : "decode-ndjson" ,
} ,
{
description : "empty string" ,
skipDoc : true ,
input : ` "" ` ,
2023-10-18 01:11:53 +00:00
expected : "\n" ,
2022-08-01 06:26:43 +00:00
scenarioType : "decode-ndjson" ,
} ,
2022-07-27 02:26:22 +00:00
{
description : "strings" ,
skipDoc : true ,
input : ` ["", "cat"] ` ,
expected : "- \"\"\n- cat\n" ,
scenarioType : "decode-ndjson" ,
} ,
{
description : "null" ,
skipDoc : true ,
input : ` null ` ,
expected : "null\n" ,
scenarioType : "decode-ndjson" ,
} ,
{
description : "booleans" ,
skipDoc : true ,
input : ` [true, false] ` ,
expected : "- true\n- false\n" ,
scenarioType : "decode-ndjson" ,
} ,
}
func documentRoundtripNdJsonScenario ( w * bufio . Writer , s formatScenario , indent int ) {
writeOrPanic ( w , fmt . Sprintf ( "## %v\n" , s . description ) )
if s . subdescription != "" {
writeOrPanic ( w , s . subdescription )
writeOrPanic ( w , "\n\n" )
}
writeOrPanic ( w , "Given a sample.json file of:\n" )
writeOrPanic ( w , fmt . Sprintf ( "```json\n%v\n```\n" , s . input ) )
writeOrPanic ( w , "then\n" )
expression := s . expression
if expression != "" {
writeOrPanic ( w , fmt . Sprintf ( "```bash\nyq -p=json -o=json -I=%v '%v' sample.json\n```\n" , indent , expression ) )
} else {
writeOrPanic ( w , fmt . Sprintf ( "```bash\nyq -p=json -o=json -I=%v sample.json\n```\n" , indent ) )
}
writeOrPanic ( w , "will output\n" )
2022-11-10 08:21:18 +00:00
writeOrPanic ( w , fmt . Sprintf ( "```yaml\n%v```\n\n" , mustProcessFormatScenario ( s , NewJSONDecoder ( ) , NewJSONEncoder ( indent , false , false ) ) ) )
2022-07-27 02:26:22 +00:00
}
func documentDecodeNdJsonScenario ( w * bufio . Writer , s formatScenario ) {
writeOrPanic ( w , fmt . Sprintf ( "## %v\n" , s . description ) )
if s . subdescription != "" {
writeOrPanic ( w , s . subdescription )
writeOrPanic ( w , "\n\n" )
}
writeOrPanic ( w , "Given a sample.json file of:\n" )
writeOrPanic ( w , fmt . Sprintf ( "```json\n%v\n```\n" , s . input ) )
writeOrPanic ( w , "then\n" )
expression := s . expression
if expression != "" {
writeOrPanic ( w , fmt . Sprintf ( "```bash\nyq -p=json '%v' sample.json\n```\n" , expression ) )
} else {
writeOrPanic ( w , "```bash\nyq -p=json sample.json\n```\n" )
}
writeOrPanic ( w , "will output\n" )
2022-11-02 10:41:39 +00:00
writeOrPanic ( w , fmt . Sprintf ( "```yaml\n%v```\n\n" , mustProcessFormatScenario ( s , NewJSONDecoder ( ) , NewYamlEncoder ( s . indent , false , ConfiguredYamlPreferences ) ) ) )
2022-01-21 09:26:09 +00:00
}
2022-02-07 00:55:55 +00:00
func decodeJSON ( t * testing . T , jsonString string ) * CandidateNode {
2022-10-28 03:16:46 +00:00
docs , err := readDocument ( jsonString , "sample.json" , 0 )
2022-01-21 09:26:09 +00:00
if err != nil {
t . Error ( err )
return nil
}
2022-02-01 03:47:51 +00:00
exp , err := getExpressionParser ( ) . ParseExpression ( PrettyPrintExp )
2022-01-21 09:26:09 +00:00
if err != nil {
t . Error ( err )
return nil
}
context , err := NewDataTreeNavigator ( ) . GetMatchingNodes ( Context { MatchingNodes : docs } , exp )
if err != nil {
t . Error ( err )
return nil
}
return context . MatchingNodes . Front ( ) . Value . ( * CandidateNode )
}
2022-02-07 00:55:55 +00:00
func testJSONScenario ( t * testing . T , s formatScenario ) {
2022-06-25 02:22:03 +00:00
switch s . scenarioType {
2023-10-18 01:11:53 +00:00
case "encode" :
2022-11-10 08:21:18 +00:00
test . AssertResultWithContext ( t , s . expected , mustProcessFormatScenario ( s , NewYamlDecoder ( ConfiguredYamlPreferences ) , NewJSONEncoder ( s . indent , false , false ) ) , s . description )
2023-10-18 01:11:53 +00:00
case "decode" :
test . AssertResultWithContext ( t , s . expected , mustProcessFormatScenario ( s , NewJSONDecoder ( ) , NewJSONEncoder ( s . indent , false , false ) ) , s . description )
2022-07-27 02:26:22 +00:00
case "decode-ndjson" :
2022-11-02 10:41:39 +00:00
test . AssertResultWithContext ( t , s . expected , mustProcessFormatScenario ( s , NewJSONDecoder ( ) , NewYamlEncoder ( 2 , false , ConfiguredYamlPreferences ) ) , s . description )
2022-07-27 02:26:22 +00:00
case "roundtrip-ndjson" :
2022-11-10 08:21:18 +00:00
test . AssertResultWithContext ( t , s . expected , mustProcessFormatScenario ( s , NewJSONDecoder ( ) , NewJSONEncoder ( 0 , false , false ) ) , s . description )
2022-07-27 02:26:22 +00:00
case "roundtrip-multi" :
2022-11-10 08:21:18 +00:00
test . AssertResultWithContext ( t , s . expected , mustProcessFormatScenario ( s , NewJSONDecoder ( ) , NewJSONEncoder ( 2 , false , false ) ) , s . description )
2022-11-10 11:33:38 +00:00
case "decode-error" :
result , err := processFormatScenario ( s , NewJSONDecoder ( ) , NewJSONEncoder ( 2 , false , false ) )
if err == nil {
t . Errorf ( "Expected error '%v' but it worked: %v" , s . expectedError , result )
} else {
test . AssertResultComplexWithContext ( t , s . expectedError , err . Error ( ) , s . description )
}
2022-06-25 02:22:03 +00:00
default :
panic ( fmt . Sprintf ( "unhandled scenario type %q" , s . scenarioType ) )
2022-01-21 09:26:09 +00:00
}
}
2022-02-07 00:55:55 +00:00
func documentJSONDecodeScenario ( t * testing . T , w * bufio . Writer , s formatScenario ) {
2022-01-21 09:26:09 +00:00
writeOrPanic ( w , fmt . Sprintf ( "## %v\n" , s . description ) )
if s . subdescription != "" {
writeOrPanic ( w , s . subdescription )
writeOrPanic ( w , "\n\n" )
}
writeOrPanic ( w , "Given a sample.json file of:\n" )
writeOrPanic ( w , fmt . Sprintf ( "```json\n%v\n```\n" , s . input ) )
writeOrPanic ( w , "then\n" )
2022-01-27 06:21:10 +00:00
writeOrPanic ( w , "```bash\nyq -P '.' sample.json\n```\n" )
2022-01-21 09:26:09 +00:00
writeOrPanic ( w , "will output\n" )
var output bytes . Buffer
printer := NewSimpleYamlPrinter ( bufio . NewWriter ( & output ) , YamlOutputFormat , true , false , 2 , true )
2022-02-07 00:55:55 +00:00
node := decodeJSON ( t , s . input )
2022-01-21 09:26:09 +00:00
err := printer . PrintResults ( node . AsList ( ) )
if err != nil {
t . Error ( err )
return
}
writeOrPanic ( w , fmt . Sprintf ( "```yaml\n%v```\n\n" , output . String ( ) ) )
}
2022-02-07 00:55:55 +00:00
func documentJSONScenario ( t * testing . T , w * bufio . Writer , i interface { } ) {
2022-01-21 09:26:09 +00:00
s := i . ( formatScenario )
if s . skipDoc {
return
}
2022-06-25 02:22:03 +00:00
switch s . scenarioType {
case "" :
2022-02-07 00:55:55 +00:00
documentJSONDecodeScenario ( t , w , s )
2022-06-25 02:22:03 +00:00
case "encode" :
documentJSONEncodeScenario ( w , s )
2022-07-27 02:26:22 +00:00
case "decode-ndjson" :
documentDecodeNdJsonScenario ( w , s )
case "roundtrip-ndjson" :
documentRoundtripNdJsonScenario ( w , s , 0 )
case "roundtrip-multi" :
documentRoundtripNdJsonScenario ( w , s , 2 )
2022-06-25 02:22:03 +00:00
default :
panic ( fmt . Sprintf ( "unhandled scenario type %q" , s . scenarioType ) )
2022-01-21 09:26:09 +00:00
}
}
2022-02-07 00:55:55 +00:00
func documentJSONEncodeScenario ( w * bufio . Writer , s formatScenario ) {
2022-01-21 09:26:09 +00:00
writeOrPanic ( w , fmt . Sprintf ( "## %v\n" , s . description ) )
if s . subdescription != "" {
writeOrPanic ( w , s . subdescription )
writeOrPanic ( w , "\n\n" )
}
writeOrPanic ( w , "Given a sample.yml file of:\n" )
writeOrPanic ( w , fmt . Sprintf ( "```yaml\n%v\n```\n" , s . input ) )
writeOrPanic ( w , "then\n" )
expression := s . expression
if expression == "" {
expression = "."
}
if s . indent == 2 {
2022-01-27 06:21:10 +00:00
writeOrPanic ( w , fmt . Sprintf ( "```bash\nyq -o=json '%v' sample.yml\n```\n" , expression ) )
2022-01-21 09:26:09 +00:00
} else {
2022-01-27 06:21:10 +00:00
writeOrPanic ( w , fmt . Sprintf ( "```bash\nyq -o=json -I=%v '%v' sample.yml\n```\n" , s . indent , expression ) )
2022-01-21 09:26:09 +00:00
}
writeOrPanic ( w , "will output\n" )
2022-11-10 08:21:18 +00:00
writeOrPanic ( w , fmt . Sprintf ( "```json\n%v```\n\n" , mustProcessFormatScenario ( s , NewYamlDecoder ( ConfiguredYamlPreferences ) , NewJSONEncoder ( s . indent , false , false ) ) ) )
2022-01-21 09:26:09 +00:00
}
2022-02-07 00:55:55 +00:00
func TestJSONScenarios ( t * testing . T ) {
2022-01-21 09:26:09 +00:00
for _ , tt := range jsonScenarios {
2022-02-07 00:55:55 +00:00
testJSONScenario ( t , tt )
2022-01-21 09:26:09 +00:00
}
genericScenarios := make ( [ ] interface { } , len ( jsonScenarios ) )
for i , s := range jsonScenarios {
genericScenarios [ i ] = s
}
2022-02-07 00:55:55 +00:00
documentScenarios ( t , "usage" , "convert" , genericScenarios , documentJSONScenario )
2022-01-21 09:26:09 +00:00
}