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:",inline"`
   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:",inline"`
   198  			} `json:",inline"`
   199  			X map[string]jsontext.Value `json:",inline"`
   200  		}{},
   201  		want: structFields{
   202  			inlinedFallback: &structField{id: 0, index: []int{2}, typ: T[map[string]jsontext.Value](), fieldOptions: fieldOptions{name: "X", quotedName: `"X"`, inline: 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("InlineWithOptions"),
   236  		in: struct {
   237  			A struct{} `json:",inline,omitempty"`
   238  		}{},
   239  		wantErr: errors.New("Go struct field A cannot have any options other than `inline` specified"),
   240  	}, {
   241  		name: jsontest.Name("UnknownWithOptions"),
   242  		in: struct {
   243  			A map[string]any `json:",inline,omitempty"`
   244  		}{},
   245  		want: structFields{inlinedFallback: &structField{
   246  			index: []int{0},
   247  			typ:   reflect.TypeFor[map[string]any](),
   248  			fieldOptions: fieldOptions{
   249  				name:       "A",
   250  				quotedName: `"A"`,
   251  				inline:     true,
   252  			},
   253  		}},
   254  		wantErr: errors.New("Go struct field A cannot have any options other than `inline` specified"),
   255  	}, {
   256  		name: jsontest.Name("InlineTextMarshaler"),
   257  		in: struct {
   258  			A struct{ encoding.TextMarshaler } `json:",inline"`
   259  		}{},
   260  		want: structFields{flattened: []structField{{
   261  			index: []int{0, 0},
   262  			typ:   reflect.TypeFor[encoding.TextMarshaler](),
   263  			fieldOptions: fieldOptions{
   264  				name:       "TextMarshaler",
   265  				quotedName: `"TextMarshaler"`,
   266  			},
   267  		}}},
   268  		wantErr: errors.New(`inlined Go struct field A of type struct { encoding.TextMarshaler } must not implement marshal or unmarshal methods`),
   269  	}, {
   270  		name: jsontest.Name("InlineTextAppender"),
   271  		in: struct {
   272  			A struct{ encoding.TextAppender } `json:",inline"`
   273  		}{},
   274  		want: structFields{flattened: []structField{{
   275  			index: []int{0, 0},
   276  			typ:   reflect.TypeFor[encoding.TextAppender](),
   277  			fieldOptions: fieldOptions{
   278  				name:       "TextAppender",
   279  				quotedName: `"TextAppender"`,
   280  			},
   281  		}}},
   282  		wantErr: errors.New(`inlined Go struct field A of type struct { encoding.TextAppender } must not implement marshal or unmarshal methods`),
   283  	}, {
   284  		name: jsontest.Name("InlineJSONMarshaler"),
   285  		in: struct {
   286  			A struct{ Marshaler } `json:",inline"`
   287  		}{},
   288  		want: structFields{flattened: []structField{{
   289  			index: []int{0, 0},
   290  			typ:   reflect.TypeFor[Marshaler](),
   291  			fieldOptions: fieldOptions{
   292  				name:       "Marshaler",
   293  				quotedName: `"Marshaler"`,
   294  			},
   295  		}}},
   296  		wantErr: errors.New(`inlined Go struct field A of type struct { json.Marshaler } must not implement marshal or unmarshal methods`),
   297  	}, {
   298  		name: jsontest.Name("InlineJSONMarshalerTo"),
   299  		in: struct {
   300  			A struct{ MarshalerTo } `json:",inline"`
   301  		}{},
   302  		want: structFields{flattened: []structField{{
   303  			index: []int{0, 0},
   304  			typ:   reflect.TypeFor[MarshalerTo](),
   305  			fieldOptions: fieldOptions{
   306  				name:       "MarshalerTo",
   307  				quotedName: `"MarshalerTo"`,
   308  			},
   309  		}}},
   310  		wantErr: errors.New(`inlined Go struct field A of type struct { json.MarshalerTo } must not implement marshal or unmarshal methods`),
   311  	}, {
   312  		name: jsontest.Name("InlineTextUnmarshaler"),
   313  		in: struct {
   314  			A *struct{ encoding.TextUnmarshaler } `json:",inline"`
   315  		}{},
   316  		want: structFields{flattened: []structField{{
   317  			index: []int{0, 0},
   318  			typ:   reflect.TypeFor[encoding.TextUnmarshaler](),
   319  			fieldOptions: fieldOptions{
   320  				name:       "TextUnmarshaler",
   321  				quotedName: `"TextUnmarshaler"`,
   322  			},
   323  		}}},
   324  		wantErr: errors.New(`inlined Go struct field A of type struct { encoding.TextUnmarshaler } must not implement marshal or unmarshal methods`),
   325  	}, {
   326  		name: jsontest.Name("InlineJSONUnmarshaler"),
   327  		in: struct {
   328  			A *struct{ Unmarshaler } `json:",inline"`
   329  		}{},
   330  		want: structFields{flattened: []structField{{
   331  			index: []int{0, 0},
   332  			typ:   reflect.TypeFor[Unmarshaler](),
   333  			fieldOptions: fieldOptions{
   334  				name:       "Unmarshaler",
   335  				quotedName: `"Unmarshaler"`,
   336  			},
   337  		}}},
   338  		wantErr: errors.New(`inlined Go struct field A of type struct { json.Unmarshaler } must not implement marshal or unmarshal methods`),
   339  	}, {
   340  		name: jsontest.Name("InlineJSONUnmarshalerFrom"),
   341  		in: struct {
   342  			A struct{ UnmarshalerFrom } `json:",inline"`
   343  		}{},
   344  		want: structFields{flattened: []structField{{
   345  			index: []int{0, 0},
   346  			typ:   reflect.TypeFor[UnmarshalerFrom](),
   347  			fieldOptions: fieldOptions{
   348  				name:       "UnmarshalerFrom",
   349  				quotedName: `"UnmarshalerFrom"`,
   350  			},
   351  		}}},
   352  		wantErr: errors.New(`inlined Go struct field A of type struct { json.UnmarshalerFrom } must not implement marshal or unmarshal methods`),
   353  	}, {
   354  		name: jsontest.Name("InlineUnsupported/MapIntKey"),
   355  		in: struct {
   356  			A map[int]any `json:",inline"`
   357  		}{},
   358  		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`),
   359  	}, {
   360  		name: jsontest.Name("InlineUnsupported/MapTextMarshalerStringKey"),
   361  		in: struct {
   362  			A map[nocaseString]any `json:",inline"`
   363  		}{},
   364  		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`),
   365  	}, {
   366  		name: jsontest.Name("InlineUnsupported/MapMarshalerStringKey"),
   367  		in: struct {
   368  			A map[stringMarshalEmpty]any `json:",inline"`
   369  		}{},
   370  		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`),
   371  	}, {
   372  		name: jsontest.Name("InlineUnsupported/DoublePointer"),
   373  		in: struct {
   374  			A **struct{} `json:",inline"`
   375  		}{},
   376  		wantErr: errors.New(`inlined Go struct field A of type *struct {} must be a Go struct, Go map of string key, or jsontext.Value`),
   377  	}, {
   378  		name: jsontest.Name("DuplicateInline"),
   379  		in: struct {
   380  			A map[string]any `json:",inline"`
   381  			B jsontext.Value `json:",inline"`
   382  		}{},
   383  		wantErr: errors.New(`inlined Go struct fields A and B cannot both be a Go map or jsontext.Value`),
   384  	}, {
   385  		name: jsontest.Name("DuplicateEmbedInline"),
   386  		in: struct {
   387  			A MapStringAny   `json:",inline"`
   388  			B jsontext.Value `json:",inline"`
   389  		}{},
   390  		wantErr: errors.New(`inlined Go struct fields A and B cannot both be a Go map or jsontext.Value`),
   391  	}}
   392  
   393  	for _, tt := range tests {
   394  		t.Run(tt.name.Name, func(t *testing.T) {
   395  			got, err := makeStructFields(reflect.TypeOf(tt.in))
   396  
   397  			// Sanity check that pointers are consistent.
   398  			pointers := make(map[*structField]bool)
   399  			for i := range got.flattened {
   400  				pointers[&got.flattened[i]] = true
   401  			}
   402  			for _, f := range got.byActualName {
   403  				if !pointers[f] {
   404  					t.Errorf("%s: byActualName pointer not in flattened", tt.name.Where)
   405  				}
   406  			}
   407  			for _, fs := range got.byFoldedName {
   408  				for _, f := range fs {
   409  					if !pointers[f] {
   410  						t.Errorf("%s: byFoldedName pointer not in flattened", tt.name.Where)
   411  					}
   412  				}
   413  			}
   414  
   415  			// Zero out fields that are incomparable.
   416  			for i := range got.flattened {
   417  				got.flattened[i].fncs = nil
   418  				got.flattened[i].isEmpty = nil
   419  			}
   420  			if got.inlinedFallback != nil {
   421  				got.inlinedFallback.fncs = nil
   422  				got.inlinedFallback.isEmpty = nil
   423  			}
   424  
   425  			// Reproduce maps in want.
   426  			tt.want.byActualName = make(map[string]*structField)
   427  			for i := range tt.want.flattened {
   428  				f := &tt.want.flattened[i]
   429  				tt.want.byActualName[f.name] = f
   430  			}
   431  			tt.want.byFoldedName = make(map[string][]*structField)
   432  			for i, f := range tt.want.flattened {
   433  				foldedName := string(foldName([]byte(f.name)))
   434  				tt.want.byFoldedName[foldedName] = append(tt.want.byFoldedName[foldedName], &tt.want.flattened[i])
   435  			}
   436  
   437  			// Only compare underlying error to simplify test logic.
   438  			var gotErr error
   439  			if err != nil {
   440  				gotErr = err.Err
   441  			}
   442  
   443  			tt.want.reindex()
   444  			if !reflect.DeepEqual(got, tt.want) || !reflect.DeepEqual(gotErr, tt.wantErr) {
   445  				t.Errorf("%s: makeStructFields(%T):\n\tgot  (%v, %v)\n\twant (%v, %v)", tt.name.Where, tt.in, got, gotErr, tt.want, tt.wantErr)
   446  			}
   447  		})
   448  	}
   449  }
   450  
   451  func TestParseTagOptions(t *testing.T) {
   452  	tests := []struct {
   453  		name        jsontest.CaseName
   454  		in          any // must be a struct with a single field
   455  		wantOpts    fieldOptions
   456  		wantIgnored bool
   457  		wantErr     error
   458  	}{{
   459  		name: jsontest.Name("GoName"),
   460  		in: struct {
   461  			FieldName int
   462  		}{},
   463  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
   464  	}, {
   465  		name: jsontest.Name("GoNameWithOptions"),
   466  		in: struct {
   467  			FieldName int `json:",inline"`
   468  		}{},
   469  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, inline: true},
   470  	}, {
   471  		name: jsontest.Name("Empty"),
   472  		in: struct {
   473  			V int `json:""`
   474  		}{},
   475  		wantOpts: fieldOptions{name: "V", quotedName: `"V"`},
   476  	}, {
   477  		name: jsontest.Name("Unexported"),
   478  		in: struct {
   479  			v int `json:"Hello"`
   480  		}{},
   481  		wantIgnored: true,
   482  		wantErr:     errors.New("unexported Go struct field v cannot have non-ignored `json:\"Hello\"` tag"),
   483  	}, {
   484  		name: jsontest.Name("UnexportedEmpty"),
   485  		in: struct {
   486  			v int `json:""`
   487  		}{},
   488  		wantIgnored: true,
   489  		wantErr:     errors.New("unexported Go struct field v cannot have non-ignored `json:\"\"` tag"),
   490  	}, {
   491  		name: jsontest.Name("EmbedUnexported"),
   492  		in: struct {
   493  			unexported
   494  		}{},
   495  		wantOpts: fieldOptions{name: "unexported", quotedName: `"unexported"`},
   496  	}, {
   497  		name: jsontest.Name("Ignored"),
   498  		in: struct {
   499  			V int `json:"-"`
   500  		}{},
   501  		wantIgnored: true,
   502  	}, {
   503  		name: jsontest.Name("IgnoredEmbedUnexported"),
   504  		in: struct {
   505  			unexported `json:"-"`
   506  		}{},
   507  		wantIgnored: true,
   508  	}, {
   509  		name: jsontest.Name("DashComma"),
   510  		in: struct {
   511  			V int `json:"-,"`
   512  		}{},
   513  		wantOpts: fieldOptions{hasName: true, name: "-", quotedName: `"-"`},
   514  		wantErr:  errors.New("Go struct field V has malformed `json` tag: invalid trailing ',' character"),
   515  	}, {
   516  		name: jsontest.Name("DashCommaOmitEmpty"),
   517  		in: struct {
   518  			V int `json:"-,omitempty"`
   519  		}{},
   520  		wantOpts: fieldOptions{hasName: true, name: "-", quotedName: `"-"`, omitempty: true},
   521  		wantErr:  errors.New("Go struct field V has JSON object name \"-\"; either use `json:\"-\"` to ignore the field or use `json:\"'-',omitempty\"` to specify \"-\" as the name"),
   522  	}, {
   523  		name: jsontest.Name("QuotedDashCommaOmitEmpty"),
   524  		in: struct {
   525  			V int `json:"'-',omitempty"`
   526  		}{},
   527  		wantOpts: fieldOptions{hasName: true, name: "-", quotedName: `"-"`, omitempty: true},
   528  	}, {
   529  		name: jsontest.Name("QuotedDashName"),
   530  		in: struct {
   531  			V int `json:"'-'"`
   532  		}{},
   533  		wantOpts: fieldOptions{hasName: true, name: "-", quotedName: `"-"`},
   534  	}, {
   535  		name: jsontest.Name("LatinPunctuationName"),
   536  		in: struct {
   537  			V int `json:"$%-/"`
   538  		}{},
   539  		wantOpts: fieldOptions{hasName: true, name: "$%-/", quotedName: `"$%-/"`},
   540  	}, {
   541  		name: jsontest.Name("QuotedLatinPunctuationName"),
   542  		in: struct {
   543  			V int `json:"'$%-/'"`
   544  		}{},
   545  		wantOpts: fieldOptions{hasName: true, name: "$%-/", quotedName: `"$%-/"`},
   546  	}, {
   547  		name: jsontest.Name("LatinDigitsName"),
   548  		in: struct {
   549  			V int `json:"0123456789"`
   550  		}{},
   551  		wantOpts: fieldOptions{hasName: true, name: "0123456789", quotedName: `"0123456789"`},
   552  	}, {
   553  		name: jsontest.Name("QuotedLatinDigitsName"),
   554  		in: struct {
   555  			V int `json:"'0123456789'"`
   556  		}{},
   557  		wantOpts: fieldOptions{hasName: true, name: "0123456789", quotedName: `"0123456789"`},
   558  	}, {
   559  		name: jsontest.Name("LatinUppercaseName"),
   560  		in: struct {
   561  			V int `json:"ABCDEFGHIJKLMOPQRSTUVWXYZ"`
   562  		}{},
   563  		wantOpts: fieldOptions{hasName: true, name: "ABCDEFGHIJKLMOPQRSTUVWXYZ", quotedName: `"ABCDEFGHIJKLMOPQRSTUVWXYZ"`},
   564  	}, {
   565  		name: jsontest.Name("LatinLowercaseName"),
   566  		in: struct {
   567  			V int `json:"abcdefghijklmnopqrstuvwxyz_"`
   568  		}{},
   569  		wantOpts: fieldOptions{hasName: true, name: "abcdefghijklmnopqrstuvwxyz_", quotedName: `"abcdefghijklmnopqrstuvwxyz_"`},
   570  	}, {
   571  		name: jsontest.Name("GreekName"),
   572  		in: struct {
   573  			V string `json:"Ελλάδα"`
   574  		}{},
   575  		wantOpts: fieldOptions{hasName: true, name: "Ελλάδα", quotedName: `"Ελλάδα"`},
   576  	}, {
   577  		name: jsontest.Name("QuotedGreekName"),
   578  		in: struct {
   579  			V string `json:"'Ελλάδα'"`
   580  		}{},
   581  		wantOpts: fieldOptions{hasName: true, name: "Ελλάδα", quotedName: `"Ελλάδα"`},
   582  	}, {
   583  		name: jsontest.Name("ChineseName"),
   584  		in: struct {
   585  			V string `json:"世界"`
   586  		}{},
   587  		wantOpts: fieldOptions{hasName: true, name: "世界", quotedName: `"世界"`},
   588  	}, {
   589  		name: jsontest.Name("QuotedChineseName"),
   590  		in: struct {
   591  			V string `json:"'世界'"`
   592  		}{},
   593  		wantOpts: fieldOptions{hasName: true, name: "世界", quotedName: `"世界"`},
   594  	}, {
   595  		name: jsontest.Name("PercentSlashName"),
   596  		in: struct {
   597  			V int `json:"text/html%"`
   598  		}{},
   599  		wantOpts: fieldOptions{hasName: true, name: "text/html%", quotedName: `"text/html%"`},
   600  	}, {
   601  		name: jsontest.Name("QuotedPercentSlashName"),
   602  		in: struct {
   603  			V int `json:"'text/html%'"`
   604  		}{},
   605  		wantOpts: fieldOptions{hasName: true, name: "text/html%", quotedName: `"text/html%"`},
   606  	}, {
   607  		name: jsontest.Name("PunctuationName"),
   608  		in: struct {
   609  			V string `json:"!#$%&()*+-./:;<=>?@[]^_{|}~ "`
   610  		}{},
   611  		wantOpts: fieldOptions{hasName: true, name: "!#$%&()*+-./:;<=>?@[]^_{|}~ ", quotedName: `"!#$%&()*+-./:;<=>?@[]^_{|}~ "`, nameNeedEscape: true},
   612  	}, {
   613  		name: jsontest.Name("QuotedPunctuationName"),
   614  		in: struct {
   615  			V string `json:"'!#$%&()*+-./:;<=>?@[]^_{|}~ '"`
   616  		}{},
   617  		wantOpts: fieldOptions{hasName: true, name: "!#$%&()*+-./:;<=>?@[]^_{|}~ ", quotedName: `"!#$%&()*+-./:;<=>?@[]^_{|}~ "`, nameNeedEscape: true},
   618  	}, {
   619  		name: jsontest.Name("EmptyName"),
   620  		in: struct {
   621  			V int `json:"''"`
   622  		}{},
   623  		wantOpts: fieldOptions{hasName: true, name: "", quotedName: `""`},
   624  	}, {
   625  		name: jsontest.Name("SpaceName"),
   626  		in: struct {
   627  			V int `json:"' '"`
   628  		}{},
   629  		wantOpts: fieldOptions{hasName: true, name: " ", quotedName: `" "`},
   630  	}, {
   631  		name: jsontest.Name("CommaQuotes"),
   632  		in: struct {
   633  			V int `json:"',\\'\"\\\"'"`
   634  		}{},
   635  		wantOpts: fieldOptions{hasName: true, name: `,'""`, quotedName: `",'\"\""`, nameNeedEscape: true},
   636  	}, {
   637  		name: jsontest.Name("SingleComma"),
   638  		in: struct {
   639  			V int `json:","`
   640  		}{},
   641  		wantOpts: fieldOptions{name: "V", quotedName: `"V"`},
   642  		wantErr:  errors.New("Go struct field V has malformed `json` tag: invalid trailing ',' character"),
   643  	}, {
   644  		name: jsontest.Name("SuperfluousCommas"),
   645  		in: struct {
   646  			V int `json:",,,,\"\",,inline,,,,,"`
   647  		}{},
   648  		wantOpts: fieldOptions{name: "V", quotedName: `"V"`, inline: true},
   649  		wantErr:  errors.New("Go struct field V has malformed `json` tag: invalid character ',' at start of option (expecting Unicode letter or single quote)"),
   650  	}, {
   651  		name: jsontest.Name("CaseAloneOption"),
   652  		in: struct {
   653  			FieldName int `json:",case"`
   654  		}{},
   655  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
   656  		wantErr:  errors.New("Go struct field FieldName is missing value for `case` tag option; specify `case:ignore` or `case:strict` instead"),
   657  	}, {
   658  		name: jsontest.Name("CaseIgnoreOption"),
   659  		in: struct {
   660  			FieldName int `json:",case:ignore"`
   661  		}{},
   662  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, casing: caseIgnore},
   663  	}, {
   664  		name: jsontest.Name("CaseStrictOption"),
   665  		in: struct {
   666  			FieldName int `json:",case:strict"`
   667  		}{},
   668  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, casing: caseStrict},
   669  	}, {
   670  		name: jsontest.Name("CaseUnknownOption"),
   671  		in: struct {
   672  			FieldName int `json:",case:unknown"`
   673  		}{},
   674  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
   675  		wantErr:  errors.New("Go struct field FieldName has unknown `case:unknown` tag value"),
   676  	}, {
   677  		name: jsontest.Name("CaseQuotedOption"),
   678  		in: struct {
   679  			FieldName int `json:",case:'ignore'"`
   680  		}{},
   681  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, casing: caseIgnore},
   682  		wantErr:  errors.New("Go struct field FieldName has unnecessarily quoted appearance of `case:'ignore'` tag option; specify `case:ignore` instead"),
   683  	}, {
   684  		name: jsontest.Name("BothCaseOptions"),
   685  		in: struct {
   686  			FieldName int `json:",case:ignore,case:strict"`
   687  		}{},
   688  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, casing: caseIgnore | caseStrict},
   689  		wantErr:  errors.New("Go struct field FieldName cannot have both `case:ignore` and `case:strict` tag options"),
   690  	}, {
   691  		name: jsontest.Name("InlineOption"),
   692  		in: struct {
   693  			FieldName int `json:",inline"`
   694  		}{},
   695  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, inline: true},
   696  	}, {
   697  		name: jsontest.Name("OmitZeroOption"),
   698  		in: struct {
   699  			FieldName int `json:",omitzero"`
   700  		}{},
   701  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, omitzero: true},
   702  	}, {
   703  		name: jsontest.Name("OmitEmptyOption"),
   704  		in: struct {
   705  			FieldName int `json:",omitempty"`
   706  		}{},
   707  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, omitempty: true},
   708  	}, {
   709  		name: jsontest.Name("StringOption"),
   710  		in: struct {
   711  			FieldName int `json:",string"`
   712  		}{},
   713  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, string: true},
   714  	}, {
   715  		name: jsontest.Name("FormatOptionEqual"),
   716  		in: struct {
   717  			FieldName int `json:",format=fizzbuzz"`
   718  		}{},
   719  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
   720  		wantErr:  errors.New("Go struct field FieldName is missing value for `format` tag option"),
   721  	}, {
   722  		name: jsontest.Name("FormatOptionColon"),
   723  		in: struct {
   724  			FieldName int `json:",format:fizzbuzz"`
   725  		}{},
   726  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, format: "fizzbuzz"},
   727  	}, {
   728  		name: jsontest.Name("FormatOptionQuoted"),
   729  		in: struct {
   730  			FieldName int `json:",format:'2006-01-02'"`
   731  		}{},
   732  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, format: "2006-01-02"},
   733  	}, {
   734  		name: jsontest.Name("FormatOptionInvalid"),
   735  		in: struct {
   736  			FieldName int `json:",format:'2006-01-02"`
   737  		}{},
   738  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
   739  		wantErr:  errors.New("Go struct field FieldName has malformed value for `format` tag option: single-quoted string not terminated: '2006-01-0..."),
   740  	}, {
   741  		name: jsontest.Name("FormatOptionNotLast"),
   742  		in: struct {
   743  			FieldName int `json:",format:alpha,ordered"`
   744  		}{},
   745  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, format: "alpha"},
   746  		wantErr:  errors.New("Go struct field FieldName has `format` tag option that was not specified last"),
   747  	}, {
   748  		name: jsontest.Name("AllOptions"),
   749  		in: struct {
   750  			FieldName int `json:",case:ignore,inline,omitzero,omitempty,string,format:format"`
   751  		}{},
   752  		wantOpts: fieldOptions{
   753  			name:       "FieldName",
   754  			quotedName: `"FieldName"`,
   755  			casing:     caseIgnore,
   756  			inline:     true,
   757  			omitzero:   true,
   758  			omitempty:  true,
   759  			string:     true,
   760  			format:     "format",
   761  		},
   762  	}, {
   763  		name: jsontest.Name("AllOptionsQuoted"),
   764  		in: struct {
   765  			FieldName int `json:",'case':'ignore','inline','omitzero','omitempty','string','format':'format'"`
   766  		}{},
   767  		wantOpts: fieldOptions{
   768  			name:       "FieldName",
   769  			quotedName: `"FieldName"`,
   770  			casing:     caseIgnore,
   771  			inline:     true,
   772  			omitzero:   true,
   773  			omitempty:  true,
   774  			string:     true,
   775  			format:     "format",
   776  		},
   777  		wantErr: errors.New("Go struct field FieldName has unnecessarily quoted appearance of `'case'` tag option; specify `case` instead"),
   778  	}, {
   779  		name: jsontest.Name("AllOptionsCaseSensitive"),
   780  		in: struct {
   781  			FieldName int `json:",CASE:IGNORE,INLINE,UNKNOWN,OMITZERO,OMITEMPTY,STRING,FORMAT:FORMAT"`
   782  		}{},
   783  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
   784  		wantErr:  errors.New("Go struct field FieldName has invalid appearance of `CASE` tag option; specify `case` instead"),
   785  	}, {
   786  		name: jsontest.Name("AllOptionsSpaceSensitive"),
   787  		in: struct {
   788  			FieldName int `json:", case:ignore , inline , omitzero , omitempty , string , format:format "`
   789  		}{},
   790  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
   791  		wantErr:  errors.New("Go struct field FieldName has malformed `json` tag: invalid character ' ' at start of option (expecting Unicode letter or single quote)"),
   792  	}, {
   793  		name: jsontest.Name("UnknownTagOption"),
   794  		in: struct {
   795  			FieldName int `json:",inline,whoknows,string"`
   796  		}{},
   797  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, inline: true, string: true},
   798  	}, {
   799  		name: jsontest.Name("MalformedQuotedString/MissingQuote"),
   800  		in: struct {
   801  			FieldName int `json:"'hello,string"`
   802  		}{},
   803  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, string: true},
   804  		wantErr:  errors.New("Go struct field FieldName has malformed `json` tag: single-quoted string not terminated: 'hello,str..."),
   805  	}, {
   806  		name: jsontest.Name("MalformedQuotedString/MissingComma"),
   807  		in: struct {
   808  			FieldName int `json:"'hello'inline,string"`
   809  		}{},
   810  		wantOpts: fieldOptions{hasName: true, name: "hello", quotedName: `"hello"`, inline: true, string: true},
   811  		wantErr:  errors.New("Go struct field FieldName has malformed `json` tag: invalid character 'i' before next option (expecting ',')"),
   812  	}, {
   813  		name: jsontest.Name("MalformedQuotedString/InvalidEscape"),
   814  		in: struct {
   815  			FieldName int `json:"'hello\\u####',inline,string"`
   816  		}{},
   817  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, inline: true, string: true},
   818  		wantErr:  errors.New("Go struct field FieldName has malformed `json` tag: invalid single-quoted string: 'hello\\u####'"),
   819  	}, {
   820  		name: jsontest.Name("MisnamedTag"),
   821  		in: struct {
   822  			V int `jsom:"Misnamed"`
   823  		}{},
   824  		wantOpts: fieldOptions{name: "V", quotedName: `"V"`},
   825  	}}
   826  
   827  	for _, tt := range tests {
   828  		t.Run(tt.name.Name, func(t *testing.T) {
   829  			fs := reflect.TypeOf(tt.in).Field(0)
   830  			gotOpts, gotIgnored, gotErr := parseFieldOptions(fs)
   831  			if !reflect.DeepEqual(gotOpts, tt.wantOpts) || gotIgnored != tt.wantIgnored || !reflect.DeepEqual(gotErr, tt.wantErr) {
   832  				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)
   833  			}
   834  		})
   835  	}
   836  }
   837  

View as plain text