Source file src/encoding/json/v2/fields_test.go

     1  // Copyright 2021 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build goexperiment.jsonv2
     6  
     7  package json
     8  
     9  import (
    10  	"encoding"
    11  	"errors"
    12  	"reflect"
    13  	"testing"
    14  
    15  	"encoding/json/internal/jsontest"
    16  	"encoding/json/jsontext"
    17  )
    18  
    19  type unexported struct{}
    20  
    21  func TestMakeStructFields(t *testing.T) {
    22  	type Embed struct {
    23  		Foo string
    24  	}
    25  	type Recursive struct {
    26  		A          string
    27  		*Recursive `json:",inline"`
    28  		B          string
    29  	}
    30  	type MapStringAny map[string]any
    31  	tests := []struct {
    32  		name    jsontest.CaseName
    33  		in      any
    34  		want    structFields
    35  		wantErr error
    36  	}{{
    37  		name: jsontest.Name("Names"),
    38  		in: struct {
    39  			F1 string
    40  			F2 string `json:"-"`
    41  			F3 string `json:"json_name"`
    42  			f3 string
    43  			F5 string `json:"json_name_nocase,case:ignore"`
    44  		}{},
    45  		want: structFields{
    46  			flattened: []structField{
    47  				{id: 0, index: []int{0}, typ: stringType, fieldOptions: fieldOptions{name: "F1", quotedName: `"F1"`}},
    48  				{id: 1, index: []int{2}, typ: stringType, fieldOptions: fieldOptions{name: "json_name", quotedName: `"json_name"`, hasName: true}},
    49  				{id: 2, index: []int{4}, typ: stringType, fieldOptions: fieldOptions{name: "json_name_nocase", quotedName: `"json_name_nocase"`, hasName: true, casing: caseIgnore}},
    50  			},
    51  		},
    52  	}, {
    53  		name: jsontest.Name("BreadthFirstSearch"),
    54  		in: struct {
    55  			L1A string
    56  			L1B struct {
    57  				L2A string
    58  				L2B struct {
    59  					L3A string
    60  				} `json:",inline"`
    61  				L2C string
    62  			} `json:",inline"`
    63  			L1C string
    64  			L1D struct {
    65  				L2D string
    66  				L2E struct {
    67  					L3B string
    68  				} `json:",inline"`
    69  				L2F string
    70  			} `json:",inline"`
    71  			L1E string
    72  		}{},
    73  		want: structFields{
    74  			flattened: []structField{
    75  				{id: 0, index: []int{0}, typ: stringType, fieldOptions: fieldOptions{name: "L1A", quotedName: `"L1A"`}},
    76  				{id: 3, index: []int{1, 0}, typ: stringType, fieldOptions: fieldOptions{name: "L2A", quotedName: `"L2A"`}},
    77  				{id: 7, index: []int{1, 1, 0}, typ: stringType, fieldOptions: fieldOptions{name: "L3A", quotedName: `"L3A"`}},
    78  				{id: 4, index: []int{1, 2}, typ: stringType, fieldOptions: fieldOptions{name: "L2C", quotedName: `"L2C"`}},
    79  				{id: 1, index: []int{2}, typ: stringType, fieldOptions: fieldOptions{name: "L1C", quotedName: `"L1C"`}},
    80  				{id: 5, index: []int{3, 0}, typ: stringType, fieldOptions: fieldOptions{name: "L2D", quotedName: `"L2D"`}},
    81  				{id: 8, index: []int{3, 1, 0}, typ: stringType, fieldOptions: fieldOptions{name: "L3B", quotedName: `"L3B"`}},
    82  				{id: 6, index: []int{3, 2}, typ: stringType, fieldOptions: fieldOptions{name: "L2F", quotedName: `"L2F"`}},
    83  				{id: 2, index: []int{4}, typ: stringType, fieldOptions: fieldOptions{name: "L1E", quotedName: `"L1E"`}},
    84  			},
    85  		},
    86  	}, {
    87  		name: jsontest.Name("NameResolution"),
    88  		in: struct {
    89  			X1 struct {
    90  				X struct {
    91  					A string // loses in precedence to A
    92  					B string // cancels out with X2.X.B
    93  					D string // loses in precedence to D
    94  				} `json:",inline"`
    95  			} `json:",inline"`
    96  			X2 struct {
    97  				X struct {
    98  					B string // cancels out with X1.X.B
    99  					C string
   100  					D string // loses in precedence to D
   101  				} `json:",inline"`
   102  			} `json:",inline"`
   103  			A string // takes precedence over X1.X.A
   104  			D string // takes precedence over X1.X.D and X2.X.D
   105  		}{},
   106  		want: structFields{
   107  			flattened: []structField{
   108  				{id: 2, index: []int{1, 0, 1}, typ: stringType, fieldOptions: fieldOptions{name: "C", quotedName: `"C"`}},
   109  				{id: 0, index: []int{2}, typ: stringType, fieldOptions: fieldOptions{name: "A", quotedName: `"A"`}},
   110  				{id: 1, index: []int{3}, typ: stringType, fieldOptions: fieldOptions{name: "D", quotedName: `"D"`}},
   111  			},
   112  		},
   113  	}, {
   114  		name: jsontest.Name("NameResolution/ExplicitNameUniquePrecedence"),
   115  		in: struct {
   116  			X1 struct {
   117  				A string // loses in precedence to X2.A
   118  			} `json:",inline"`
   119  			X2 struct {
   120  				A string `json:"A"`
   121  			} `json:",inline"`
   122  			X3 struct {
   123  				A string // loses in precedence to X2.A
   124  			} `json:",inline"`
   125  		}{},
   126  		want: structFields{
   127  			flattened: []structField{
   128  				{id: 0, index: []int{1, 0}, typ: stringType, fieldOptions: fieldOptions{hasName: true, name: "A", quotedName: `"A"`}},
   129  			},
   130  		},
   131  	}, {
   132  		name: jsontest.Name("NameResolution/ExplicitNameCancelsOut"),
   133  		in: struct {
   134  			X1 struct {
   135  				A string // loses in precedence to X2.A or X3.A
   136  			} `json:",inline"`
   137  			X2 struct {
   138  				A string `json:"A"` // cancels out with X3.A
   139  			} `json:",inline"`
   140  			X3 struct {
   141  				A string `json:"A"` // cancels out with X2.A
   142  			} `json:",inline"`
   143  		}{},
   144  		want: structFields{flattened: []structField{}},
   145  	}, {
   146  		name: jsontest.Name("Embed/Implicit"),
   147  		in: struct {
   148  			Embed
   149  		}{},
   150  		want: structFields{
   151  			flattened: []structField{
   152  				{id: 0, index: []int{0, 0}, typ: stringType, fieldOptions: fieldOptions{name: "Foo", quotedName: `"Foo"`}},
   153  			},
   154  		},
   155  	}, {
   156  		name: jsontest.Name("Embed/Explicit"),
   157  		in: struct {
   158  			Embed `json:",inline"`
   159  		}{},
   160  		want: structFields{
   161  			flattened: []structField{
   162  				{id: 0, index: []int{0, 0}, typ: stringType, fieldOptions: fieldOptions{name: "Foo", quotedName: `"Foo"`}},
   163  			},
   164  		},
   165  	}, {
   166  		name: jsontest.Name("Recursive"),
   167  		in: struct {
   168  			A         string
   169  			Recursive `json:",inline"`
   170  			C         string
   171  		}{},
   172  		want: structFields{
   173  			flattened: []structField{
   174  				{id: 0, index: []int{0}, typ: stringType, fieldOptions: fieldOptions{name: "A", quotedName: `"A"`}},
   175  				{id: 2, index: []int{1, 2}, typ: stringType, fieldOptions: fieldOptions{name: "B", quotedName: `"B"`}},
   176  				{id: 1, index: []int{2}, typ: stringType, fieldOptions: fieldOptions{name: "C", quotedName: `"C"`}},
   177  			},
   178  		},
   179  	}, {
   180  		name: jsontest.Name("InlinedFallback/Cancelation"),
   181  		in: struct {
   182  			X1 struct {
   183  				X jsontext.Value `json:",inline"`
   184  			} `json:",inline"`
   185  			X2 struct {
   186  				X map[string]any `json:",unknown"`
   187  			} `json:",inline"`
   188  		}{},
   189  		want: structFields{},
   190  	}, {
   191  		name: jsontest.Name("InlinedFallback/Precedence"),
   192  		in: struct {
   193  			X1 struct {
   194  				X jsontext.Value `json:",inline"`
   195  			} `json:",inline"`
   196  			X2 struct {
   197  				X map[string]any `json:",unknown"`
   198  			} `json:",inline"`
   199  			X map[string]jsontext.Value `json:",unknown"`
   200  		}{},
   201  		want: structFields{
   202  			inlinedFallback: &structField{id: 0, index: []int{2}, typ: T[map[string]jsontext.Value](), fieldOptions: fieldOptions{name: "X", quotedName: `"X"`, unknown: true}},
   203  		},
   204  	}, {
   205  		name: jsontest.Name("InlinedFallback/InvalidImplicit"),
   206  		in: struct {
   207  			MapStringAny
   208  		}{},
   209  		want: structFields{
   210  			flattened: []structField{
   211  				{id: 0, index: []int{0}, typ: reflect.TypeOf(MapStringAny(nil)), fieldOptions: fieldOptions{name: "MapStringAny", quotedName: `"MapStringAny"`}},
   212  			},
   213  		},
   214  		wantErr: errors.New("embedded Go struct field MapStringAny of non-struct type must be explicitly given a JSON name"),
   215  	}, {
   216  		name: jsontest.Name("InvalidUTF8"),
   217  		in: struct {
   218  			Name string `json:"'\\xde\\xad\\xbe\\xef'"`
   219  		}{},
   220  		want: structFields{
   221  			flattened: []structField{
   222  				{id: 0, index: []int{0}, typ: stringType, fieldOptions: fieldOptions{hasName: true, name: "\u07ad\ufffd\ufffd", quotedName: "\"\u07ad\ufffd\ufffd\"", nameNeedEscape: true}},
   223  			},
   224  		},
   225  		wantErr: errors.New(`Go struct field Name has JSON object name "ޭ\xbe\xef" with invalid UTF-8`),
   226  	}, {
   227  		name: jsontest.Name("DuplicateName"),
   228  		in: struct {
   229  			A string `json:"same"`
   230  			B string `json:"same"`
   231  		}{},
   232  		want:    structFields{flattened: []structField{}},
   233  		wantErr: errors.New(`Go struct fields A and B conflict over JSON object name "same"`),
   234  	}, {
   235  		name: jsontest.Name("BothInlineAndUnknown"),
   236  		in: struct {
   237  			A struct{} `json:",inline,unknown"`
   238  		}{},
   239  		wantErr: errors.New("Go struct field A cannot have both `inline` and `unknown` specified"),
   240  	}, {
   241  		name: jsontest.Name("InlineWithOptions"),
   242  		in: struct {
   243  			A struct{} `json:",inline,omitempty"`
   244  		}{},
   245  		wantErr: errors.New("Go struct field A cannot have any options other than `inline` or `unknown` specified"),
   246  	}, {
   247  		name: jsontest.Name("UnknownWithOptions"),
   248  		in: struct {
   249  			A map[string]any `json:",inline,omitempty"`
   250  		}{},
   251  		want: structFields{inlinedFallback: &structField{
   252  			index: []int{0},
   253  			typ:   reflect.TypeFor[map[string]any](),
   254  			fieldOptions: fieldOptions{
   255  				name:       "A",
   256  				quotedName: `"A"`,
   257  				inline:     true,
   258  			},
   259  		}},
   260  		wantErr: errors.New("Go struct field A cannot have any options other than `inline` or `unknown` specified"),
   261  	}, {
   262  		name: jsontest.Name("InlineTextMarshaler"),
   263  		in: struct {
   264  			A struct{ encoding.TextMarshaler } `json:",inline"`
   265  		}{},
   266  		want: structFields{flattened: []structField{{
   267  			index: []int{0, 0},
   268  			typ:   reflect.TypeFor[encoding.TextMarshaler](),
   269  			fieldOptions: fieldOptions{
   270  				name:       "TextMarshaler",
   271  				quotedName: `"TextMarshaler"`,
   272  			},
   273  		}}},
   274  		wantErr: errors.New(`inlined Go struct field A of type struct { encoding.TextMarshaler } must not implement marshal or unmarshal methods`),
   275  	}, {
   276  		name: jsontest.Name("InlineTextAppender"),
   277  		in: struct {
   278  			A struct{ encoding.TextAppender } `json:",inline"`
   279  		}{},
   280  		want: structFields{flattened: []structField{{
   281  			index: []int{0, 0},
   282  			typ:   reflect.TypeFor[encoding.TextAppender](),
   283  			fieldOptions: fieldOptions{
   284  				name:       "TextAppender",
   285  				quotedName: `"TextAppender"`,
   286  			},
   287  		}}},
   288  		wantErr: errors.New(`inlined Go struct field A of type struct { encoding.TextAppender } must not implement marshal or unmarshal methods`),
   289  	}, {
   290  		name: jsontest.Name("UnknownJSONMarshaler"),
   291  		in: struct {
   292  			A struct{ Marshaler } `json:",unknown"`
   293  		}{},
   294  		wantErr: errors.New(`inlined Go struct field A of type struct { json.Marshaler } must not implement marshal or unmarshal methods`),
   295  	}, {
   296  		name: jsontest.Name("InlineJSONMarshalerTo"),
   297  		in: struct {
   298  			A struct{ MarshalerTo } `json:",inline"`
   299  		}{},
   300  		want: structFields{flattened: []structField{{
   301  			index: []int{0, 0},
   302  			typ:   reflect.TypeFor[MarshalerTo](),
   303  			fieldOptions: fieldOptions{
   304  				name:       "MarshalerTo",
   305  				quotedName: `"MarshalerTo"`,
   306  			},
   307  		}}},
   308  		wantErr: errors.New(`inlined Go struct field A of type struct { json.MarshalerTo } must not implement marshal or unmarshal methods`),
   309  	}, {
   310  		name: jsontest.Name("UnknownTextUnmarshaler"),
   311  		in: struct {
   312  			A *struct{ encoding.TextUnmarshaler } `json:",unknown"`
   313  		}{},
   314  		wantErr: errors.New(`inlined Go struct field A of type struct { encoding.TextUnmarshaler } must not implement marshal or unmarshal methods`),
   315  	}, {
   316  		name: jsontest.Name("InlineJSONUnmarshaler"),
   317  		in: struct {
   318  			A *struct{ Unmarshaler } `json:",inline"`
   319  		}{},
   320  		want: structFields{flattened: []structField{{
   321  			index: []int{0, 0},
   322  			typ:   reflect.TypeFor[Unmarshaler](),
   323  			fieldOptions: fieldOptions{
   324  				name:       "Unmarshaler",
   325  				quotedName: `"Unmarshaler"`,
   326  			},
   327  		}}},
   328  		wantErr: errors.New(`inlined Go struct field A of type struct { json.Unmarshaler } must not implement marshal or unmarshal methods`),
   329  	}, {
   330  		name: jsontest.Name("UnknownJSONUnmarshalerFrom"),
   331  		in: struct {
   332  			A struct{ UnmarshalerFrom } `json:",unknown"`
   333  		}{},
   334  		wantErr: errors.New(`inlined Go struct field A of type struct { json.UnmarshalerFrom } must not implement marshal or unmarshal methods`),
   335  	}, {
   336  		name: jsontest.Name("UnknownStruct"),
   337  		in: struct {
   338  			A struct {
   339  				X, Y, Z string
   340  			} `json:",unknown"`
   341  		}{},
   342  		wantErr: errors.New("inlined Go struct field A of type struct { X string; Y string; Z string } with `unknown` tag must be a Go map of string key or a jsontext.Value"),
   343  	}, {
   344  		name: jsontest.Name("InlineUnsupported/MapIntKey"),
   345  		in: struct {
   346  			A map[int]any `json:",unknown"`
   347  		}{},
   348  		wantErr: errors.New(`inlined Go struct field A of type map[int]interface {} must be a Go struct, Go map of string key, or jsontext.Value`),
   349  	}, {
   350  		name: jsontest.Name("InlineUnsupported/MapTextMarshalerStringKey"),
   351  		in: struct {
   352  			A map[nocaseString]any `json:",inline"`
   353  		}{},
   354  		wantErr: errors.New(`inlined map field A of type map[json.nocaseString]interface {} must have a string key that does not implement marshal or unmarshal methods`),
   355  	}, {
   356  		name: jsontest.Name("InlineUnsupported/MapMarshalerStringKey"),
   357  		in: struct {
   358  			A map[stringMarshalEmpty]any `json:",inline"`
   359  		}{},
   360  		wantErr: errors.New(`inlined map field A of type map[json.stringMarshalEmpty]interface {} must have a string key that does not implement marshal or unmarshal methods`),
   361  	}, {
   362  		name: jsontest.Name("InlineUnsupported/DoublePointer"),
   363  		in: struct {
   364  			A **struct{} `json:",inline"`
   365  		}{},
   366  		wantErr: errors.New(`inlined Go struct field A of type *struct {} must be a Go struct, Go map of string key, or jsontext.Value`),
   367  	}, {
   368  		name: jsontest.Name("DuplicateInline"),
   369  		in: struct {
   370  			A map[string]any `json:",inline"`
   371  			B jsontext.Value `json:",inline"`
   372  		}{},
   373  		wantErr: errors.New(`inlined Go struct fields A and B cannot both be a Go map or jsontext.Value`),
   374  	}, {
   375  		name: jsontest.Name("DuplicateEmbedInline"),
   376  		in: struct {
   377  			A MapStringAny   `json:",inline"`
   378  			B jsontext.Value `json:",inline"`
   379  		}{},
   380  		wantErr: errors.New(`inlined Go struct fields A and B cannot both be a Go map or jsontext.Value`),
   381  	}}
   382  
   383  	for _, tt := range tests {
   384  		t.Run(tt.name.Name, func(t *testing.T) {
   385  			got, err := makeStructFields(reflect.TypeOf(tt.in))
   386  
   387  			// Sanity check that pointers are consistent.
   388  			pointers := make(map[*structField]bool)
   389  			for i := range got.flattened {
   390  				pointers[&got.flattened[i]] = true
   391  			}
   392  			for _, f := range got.byActualName {
   393  				if !pointers[f] {
   394  					t.Errorf("%s: byActualName pointer not in flattened", tt.name.Where)
   395  				}
   396  			}
   397  			for _, fs := range got.byFoldedName {
   398  				for _, f := range fs {
   399  					if !pointers[f] {
   400  						t.Errorf("%s: byFoldedName pointer not in flattened", tt.name.Where)
   401  					}
   402  				}
   403  			}
   404  
   405  			// Zero out fields that are incomparable.
   406  			for i := range got.flattened {
   407  				got.flattened[i].fncs = nil
   408  				got.flattened[i].isEmpty = nil
   409  			}
   410  			if got.inlinedFallback != nil {
   411  				got.inlinedFallback.fncs = nil
   412  				got.inlinedFallback.isEmpty = nil
   413  			}
   414  
   415  			// Reproduce maps in want.
   416  			tt.want.byActualName = make(map[string]*structField)
   417  			for i := range tt.want.flattened {
   418  				f := &tt.want.flattened[i]
   419  				tt.want.byActualName[f.name] = f
   420  			}
   421  			tt.want.byFoldedName = make(map[string][]*structField)
   422  			for i, f := range tt.want.flattened {
   423  				foldedName := string(foldName([]byte(f.name)))
   424  				tt.want.byFoldedName[foldedName] = append(tt.want.byFoldedName[foldedName], &tt.want.flattened[i])
   425  			}
   426  
   427  			// Only compare underlying error to simplify test logic.
   428  			var gotErr error
   429  			if err != nil {
   430  				gotErr = err.Err
   431  			}
   432  
   433  			tt.want.reindex()
   434  			if !reflect.DeepEqual(got, tt.want) || !reflect.DeepEqual(gotErr, tt.wantErr) {
   435  				t.Errorf("%s: makeStructFields(%T):\n\tgot  (%v, %v)\n\twant (%v, %v)", tt.name.Where, tt.in, got, gotErr, tt.want, tt.wantErr)
   436  			}
   437  		})
   438  	}
   439  }
   440  
   441  func TestParseTagOptions(t *testing.T) {
   442  	tests := []struct {
   443  		name        jsontest.CaseName
   444  		in          any // must be a struct with a single field
   445  		wantOpts    fieldOptions
   446  		wantIgnored bool
   447  		wantErr     error
   448  	}{{
   449  		name: jsontest.Name("GoName"),
   450  		in: struct {
   451  			FieldName int
   452  		}{},
   453  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
   454  	}, {
   455  		name: jsontest.Name("GoNameWithOptions"),
   456  		in: struct {
   457  			FieldName int `json:",inline"`
   458  		}{},
   459  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, inline: true},
   460  	}, {
   461  		name: jsontest.Name("Empty"),
   462  		in: struct {
   463  			V int `json:""`
   464  		}{},
   465  		wantOpts: fieldOptions{name: "V", quotedName: `"V"`},
   466  	}, {
   467  		name: jsontest.Name("Unexported"),
   468  		in: struct {
   469  			v int `json:"Hello"`
   470  		}{},
   471  		wantIgnored: true,
   472  		wantErr:     errors.New("unexported Go struct field v cannot have non-ignored `json:\"Hello\"` tag"),
   473  	}, {
   474  		name: jsontest.Name("UnexportedEmpty"),
   475  		in: struct {
   476  			v int `json:""`
   477  		}{},
   478  		wantIgnored: true,
   479  		wantErr:     errors.New("unexported Go struct field v cannot have non-ignored `json:\"\"` tag"),
   480  	}, {
   481  		name: jsontest.Name("EmbedUnexported"),
   482  		in: struct {
   483  			unexported
   484  		}{},
   485  		wantOpts: fieldOptions{name: "unexported", quotedName: `"unexported"`},
   486  	}, {
   487  		name: jsontest.Name("Ignored"),
   488  		in: struct {
   489  			V int `json:"-"`
   490  		}{},
   491  		wantIgnored: true,
   492  	}, {
   493  		name: jsontest.Name("IgnoredEmbedUnexported"),
   494  		in: struct {
   495  			unexported `json:"-"`
   496  		}{},
   497  		wantIgnored: true,
   498  	}, {
   499  		name: jsontest.Name("DashComma"),
   500  		in: struct {
   501  			V int `json:"-,"`
   502  		}{},
   503  		wantOpts: fieldOptions{hasName: true, name: "-", quotedName: `"-"`},
   504  		wantErr:  errors.New("Go struct field V has malformed `json` tag: invalid trailing ',' character"),
   505  	}, {
   506  		name: jsontest.Name("QuotedDashName"),
   507  		in: struct {
   508  			V int `json:"'-'"`
   509  		}{},
   510  		wantOpts: fieldOptions{hasName: true, name: "-", quotedName: `"-"`},
   511  	}, {
   512  		name: jsontest.Name("LatinPunctuationName"),
   513  		in: struct {
   514  			V int `json:"$%-/"`
   515  		}{},
   516  		wantOpts: fieldOptions{hasName: true, name: "$%-/", quotedName: `"$%-/"`},
   517  	}, {
   518  		name: jsontest.Name("QuotedLatinPunctuationName"),
   519  		in: struct {
   520  			V int `json:"'$%-/'"`
   521  		}{},
   522  		wantOpts: fieldOptions{hasName: true, name: "$%-/", quotedName: `"$%-/"`},
   523  	}, {
   524  		name: jsontest.Name("LatinDigitsName"),
   525  		in: struct {
   526  			V int `json:"0123456789"`
   527  		}{},
   528  		wantOpts: fieldOptions{hasName: true, name: "0123456789", quotedName: `"0123456789"`},
   529  	}, {
   530  		name: jsontest.Name("QuotedLatinDigitsName"),
   531  		in: struct {
   532  			V int `json:"'0123456789'"`
   533  		}{},
   534  		wantOpts: fieldOptions{hasName: true, name: "0123456789", quotedName: `"0123456789"`},
   535  	}, {
   536  		name: jsontest.Name("LatinUppercaseName"),
   537  		in: struct {
   538  			V int `json:"ABCDEFGHIJKLMOPQRSTUVWXYZ"`
   539  		}{},
   540  		wantOpts: fieldOptions{hasName: true, name: "ABCDEFGHIJKLMOPQRSTUVWXYZ", quotedName: `"ABCDEFGHIJKLMOPQRSTUVWXYZ"`},
   541  	}, {
   542  		name: jsontest.Name("LatinLowercaseName"),
   543  		in: struct {
   544  			V int `json:"abcdefghijklmnopqrstuvwxyz_"`
   545  		}{},
   546  		wantOpts: fieldOptions{hasName: true, name: "abcdefghijklmnopqrstuvwxyz_", quotedName: `"abcdefghijklmnopqrstuvwxyz_"`},
   547  	}, {
   548  		name: jsontest.Name("GreekName"),
   549  		in: struct {
   550  			V string `json:"Ελλάδα"`
   551  		}{},
   552  		wantOpts: fieldOptions{hasName: true, name: "Ελλάδα", quotedName: `"Ελλάδα"`},
   553  	}, {
   554  		name: jsontest.Name("QuotedGreekName"),
   555  		in: struct {
   556  			V string `json:"'Ελλάδα'"`
   557  		}{},
   558  		wantOpts: fieldOptions{hasName: true, name: "Ελλάδα", quotedName: `"Ελλάδα"`},
   559  	}, {
   560  		name: jsontest.Name("ChineseName"),
   561  		in: struct {
   562  			V string `json:"世界"`
   563  		}{},
   564  		wantOpts: fieldOptions{hasName: true, name: "世界", quotedName: `"世界"`},
   565  	}, {
   566  		name: jsontest.Name("QuotedChineseName"),
   567  		in: struct {
   568  			V string `json:"'世界'"`
   569  		}{},
   570  		wantOpts: fieldOptions{hasName: true, name: "世界", quotedName: `"世界"`},
   571  	}, {
   572  		name: jsontest.Name("PercentSlashName"),
   573  		in: struct {
   574  			V int `json:"text/html%"`
   575  		}{},
   576  		wantOpts: fieldOptions{hasName: true, name: "text/html%", quotedName: `"text/html%"`},
   577  	}, {
   578  		name: jsontest.Name("QuotedPercentSlashName"),
   579  		in: struct {
   580  			V int `json:"'text/html%'"`
   581  		}{},
   582  		wantOpts: fieldOptions{hasName: true, name: "text/html%", quotedName: `"text/html%"`},
   583  	}, {
   584  		name: jsontest.Name("PunctuationName"),
   585  		in: struct {
   586  			V string `json:"!#$%&()*+-./:;<=>?@[]^_{|}~ "`
   587  		}{},
   588  		wantOpts: fieldOptions{hasName: true, name: "!#$%&()*+-./:;<=>?@[]^_{|}~ ", quotedName: `"!#$%&()*+-./:;<=>?@[]^_{|}~ "`, nameNeedEscape: true},
   589  	}, {
   590  		name: jsontest.Name("QuotedPunctuationName"),
   591  		in: struct {
   592  			V string `json:"'!#$%&()*+-./:;<=>?@[]^_{|}~ '"`
   593  		}{},
   594  		wantOpts: fieldOptions{hasName: true, name: "!#$%&()*+-./:;<=>?@[]^_{|}~ ", quotedName: `"!#$%&()*+-./:;<=>?@[]^_{|}~ "`, nameNeedEscape: true},
   595  	}, {
   596  		name: jsontest.Name("EmptyName"),
   597  		in: struct {
   598  			V int `json:"''"`
   599  		}{},
   600  		wantOpts: fieldOptions{hasName: true, name: "", quotedName: `""`},
   601  	}, {
   602  		name: jsontest.Name("SpaceName"),
   603  		in: struct {
   604  			V int `json:"' '"`
   605  		}{},
   606  		wantOpts: fieldOptions{hasName: true, name: " ", quotedName: `" "`},
   607  	}, {
   608  		name: jsontest.Name("CommaQuotes"),
   609  		in: struct {
   610  			V int `json:"',\\'\"\\\"'"`
   611  		}{},
   612  		wantOpts: fieldOptions{hasName: true, name: `,'""`, quotedName: `",'\"\""`, nameNeedEscape: true},
   613  	}, {
   614  		name: jsontest.Name("SingleComma"),
   615  		in: struct {
   616  			V int `json:","`
   617  		}{},
   618  		wantOpts: fieldOptions{name: "V", quotedName: `"V"`},
   619  		wantErr:  errors.New("Go struct field V has malformed `json` tag: invalid trailing ',' character"),
   620  	}, {
   621  		name: jsontest.Name("SuperfluousCommas"),
   622  		in: struct {
   623  			V int `json:",,,,\"\",,inline,unknown,,,,"`
   624  		}{},
   625  		wantOpts: fieldOptions{name: "V", quotedName: `"V"`, inline: true, unknown: true},
   626  		wantErr:  errors.New("Go struct field V has malformed `json` tag: invalid character ',' at start of option (expecting Unicode letter or single quote)"),
   627  	}, {
   628  		name: jsontest.Name("CaseAloneOption"),
   629  		in: struct {
   630  			FieldName int `json:",case"`
   631  		}{},
   632  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
   633  		wantErr:  errors.New("Go struct field FieldName is missing value for `case` tag option; specify `case:ignore` or `case:strict` instead"),
   634  	}, {
   635  		name: jsontest.Name("CaseIgnoreOption"),
   636  		in: struct {
   637  			FieldName int `json:",case:ignore"`
   638  		}{},
   639  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, casing: caseIgnore},
   640  	}, {
   641  		name: jsontest.Name("CaseStrictOption"),
   642  		in: struct {
   643  			FieldName int `json:",case:strict"`
   644  		}{},
   645  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, casing: caseStrict},
   646  	}, {
   647  		name: jsontest.Name("CaseUnknownOption"),
   648  		in: struct {
   649  			FieldName int `json:",case:unknown"`
   650  		}{},
   651  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
   652  		wantErr:  errors.New("Go struct field FieldName has unknown `case:unknown` tag value"),
   653  	}, {
   654  		name: jsontest.Name("CaseQuotedOption"),
   655  		in: struct {
   656  			FieldName int `json:",case:'ignore'"`
   657  		}{},
   658  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, casing: caseIgnore},
   659  		wantErr:  errors.New("Go struct field FieldName has unnecessarily quoted appearance of `case:'ignore'` tag option; specify `case:ignore` instead"),
   660  	}, {
   661  		name: jsontest.Name("BothCaseOptions"),
   662  		in: struct {
   663  			FieldName int `json:",case:ignore,case:strict"`
   664  		}{},
   665  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, casing: caseIgnore | caseStrict},
   666  		wantErr:  errors.New("Go struct field FieldName cannot have both `case:ignore` and `case:strict` tag options"),
   667  	}, {
   668  		name: jsontest.Name("InlineOption"),
   669  		in: struct {
   670  			FieldName int `json:",inline"`
   671  		}{},
   672  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, inline: true},
   673  	}, {
   674  		name: jsontest.Name("UnknownOption"),
   675  		in: struct {
   676  			FieldName int `json:",unknown"`
   677  		}{},
   678  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, unknown: true},
   679  	}, {
   680  		name: jsontest.Name("OmitZeroOption"),
   681  		in: struct {
   682  			FieldName int `json:",omitzero"`
   683  		}{},
   684  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, omitzero: true},
   685  	}, {
   686  		name: jsontest.Name("OmitEmptyOption"),
   687  		in: struct {
   688  			FieldName int `json:",omitempty"`
   689  		}{},
   690  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, omitempty: true},
   691  	}, {
   692  		name: jsontest.Name("StringOption"),
   693  		in: struct {
   694  			FieldName int `json:",string"`
   695  		}{},
   696  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, string: true},
   697  	}, {
   698  		name: jsontest.Name("FormatOptionEqual"),
   699  		in: struct {
   700  			FieldName int `json:",format=fizzbuzz"`
   701  		}{},
   702  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
   703  		wantErr:  errors.New("Go struct field FieldName is missing value for `format` tag option"),
   704  	}, {
   705  		name: jsontest.Name("FormatOptionColon"),
   706  		in: struct {
   707  			FieldName int `json:",format:fizzbuzz"`
   708  		}{},
   709  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, format: "fizzbuzz"},
   710  	}, {
   711  		name: jsontest.Name("FormatOptionQuoted"),
   712  		in: struct {
   713  			FieldName int `json:",format:'2006-01-02'"`
   714  		}{},
   715  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, format: "2006-01-02"},
   716  	}, {
   717  		name: jsontest.Name("FormatOptionInvalid"),
   718  		in: struct {
   719  			FieldName int `json:",format:'2006-01-02"`
   720  		}{},
   721  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
   722  		wantErr:  errors.New("Go struct field FieldName has malformed value for `format` tag option: single-quoted string not terminated: '2006-01-0..."),
   723  	}, {
   724  		name: jsontest.Name("FormatOptionNotLast"),
   725  		in: struct {
   726  			FieldName int `json:",format:alpha,ordered"`
   727  		}{},
   728  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, format: "alpha"},
   729  		wantErr:  errors.New("Go struct field FieldName has `format` tag option that was not specified last"),
   730  	}, {
   731  		name: jsontest.Name("AllOptions"),
   732  		in: struct {
   733  			FieldName int `json:",case:ignore,inline,unknown,omitzero,omitempty,string,format:format"`
   734  		}{},
   735  		wantOpts: fieldOptions{
   736  			name:       "FieldName",
   737  			quotedName: `"FieldName"`,
   738  			casing:     caseIgnore,
   739  			inline:     true,
   740  			unknown:    true,
   741  			omitzero:   true,
   742  			omitempty:  true,
   743  			string:     true,
   744  			format:     "format",
   745  		},
   746  	}, {
   747  		name: jsontest.Name("AllOptionsQuoted"),
   748  		in: struct {
   749  			FieldName int `json:",'case':'ignore','inline','unknown','omitzero','omitempty','string','format':'format'"`
   750  		}{},
   751  		wantOpts: fieldOptions{
   752  			name:       "FieldName",
   753  			quotedName: `"FieldName"`,
   754  			casing:     caseIgnore,
   755  			inline:     true,
   756  			unknown:    true,
   757  			omitzero:   true,
   758  			omitempty:  true,
   759  			string:     true,
   760  			format:     "format",
   761  		},
   762  		wantErr: errors.New("Go struct field FieldName has unnecessarily quoted appearance of `'case'` tag option; specify `case` instead"),
   763  	}, {
   764  		name: jsontest.Name("AllOptionsCaseSensitive"),
   765  		in: struct {
   766  			FieldName int `json:",CASE:IGNORE,INLINE,UNKNOWN,OMITZERO,OMITEMPTY,STRING,FORMAT:FORMAT"`
   767  		}{},
   768  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
   769  		wantErr:  errors.New("Go struct field FieldName has invalid appearance of `CASE` tag option; specify `case` instead"),
   770  	}, {
   771  		name: jsontest.Name("AllOptionsSpaceSensitive"),
   772  		in: struct {
   773  			FieldName int `json:", case:ignore , inline , unknown , omitzero , omitempty , string , format:format "`
   774  		}{},
   775  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
   776  		wantErr:  errors.New("Go struct field FieldName has malformed `json` tag: invalid character ' ' at start of option (expecting Unicode letter or single quote)"),
   777  	}, {
   778  		name: jsontest.Name("UnknownTagOption"),
   779  		in: struct {
   780  			FieldName int `json:",inline,whoknows,string"`
   781  		}{},
   782  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, inline: true, string: true},
   783  	}, {
   784  		name: jsontest.Name("MalformedQuotedString/MissingQuote"),
   785  		in: struct {
   786  			FieldName int `json:"'hello,string"`
   787  		}{},
   788  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, string: true},
   789  		wantErr:  errors.New("Go struct field FieldName has malformed `json` tag: single-quoted string not terminated: 'hello,str..."),
   790  	}, {
   791  		name: jsontest.Name("MalformedQuotedString/MissingComma"),
   792  		in: struct {
   793  			FieldName int `json:"'hello'inline,string"`
   794  		}{},
   795  		wantOpts: fieldOptions{hasName: true, name: "hello", quotedName: `"hello"`, inline: true, string: true},
   796  		wantErr:  errors.New("Go struct field FieldName has malformed `json` tag: invalid character 'i' before next option (expecting ',')"),
   797  	}, {
   798  		name: jsontest.Name("MalformedQuotedString/InvalidEscape"),
   799  		in: struct {
   800  			FieldName int `json:"'hello\\u####',inline,string"`
   801  		}{},
   802  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, inline: true, string: true},
   803  		wantErr:  errors.New("Go struct field FieldName has malformed `json` tag: invalid single-quoted string: 'hello\\u####'"),
   804  	}, {
   805  		name: jsontest.Name("MisnamedTag"),
   806  		in: struct {
   807  			V int `jsom:"Misnamed"`
   808  		}{},
   809  		wantOpts: fieldOptions{name: "V", quotedName: `"V"`},
   810  	}}
   811  
   812  	for _, tt := range tests {
   813  		t.Run(tt.name.Name, func(t *testing.T) {
   814  			fs := reflect.TypeOf(tt.in).Field(0)
   815  			gotOpts, gotIgnored, gotErr := parseFieldOptions(fs)
   816  			if !reflect.DeepEqual(gotOpts, tt.wantOpts) || gotIgnored != tt.wantIgnored || !reflect.DeepEqual(gotErr, tt.wantErr) {
   817  				t.Errorf("%s: parseFieldOptions(%T) = (\n\t%v,\n\t%v,\n\t%v\n), want (\n\t%v,\n\t%v,\n\t%v\n)", tt.name.Where, tt.in, gotOpts, gotIgnored, gotErr, tt.wantOpts, tt.wantIgnored, tt.wantErr)
   818  			}
   819  		})
   820  	}
   821  }
   822  

View as plain text