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:",embed"`
    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:",embed"`
    61  				L2C string
    62  			} `json:",embed"`
    63  			L1C string
    64  			L1D struct {
    65  				L2D string
    66  				L2E struct {
    67  					L3B string
    68  				} `json:",embed"`
    69  				L2F string
    70  			} `json:",embed"`
    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:",embed"`
    95  			} `json:",embed"`
    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:",embed"`
   102  			} `json:",embed"`
   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:",embed"`
   119  			X2 struct {
   120  				A string `json:"A"`
   121  			} `json:",embed"`
   122  			X3 struct {
   123  				A string // loses in precedence to X2.A
   124  			} `json:",embed"`
   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:",embed"`
   137  			X2 struct {
   138  				A string `json:"A"` // cancels out with X3.A
   139  			} `json:",embed"`
   140  			X3 struct {
   141  				A string `json:"A"` // cancels out with X2.A
   142  			} `json:",embed"`
   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:",embed"`
   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:",embed"`
   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("EmbeddedFallback/Cancelation"),
   181  		in: struct {
   182  			X1 struct {
   183  				X jsontext.Value `json:",embed"`
   184  			} `json:",embed"`
   185  			X2 struct {
   186  				X map[string]any `json:",embed"`
   187  			} `json:",embed"`
   188  		}{},
   189  		want: structFields{},
   190  	}, {
   191  		name: jsontest.Name("EmbeddedFallback/Precedence"),
   192  		in: struct {
   193  			X1 struct {
   194  				X jsontext.Value `json:",embed"`
   195  			} `json:",embed"`
   196  			X2 struct {
   197  				X map[string]any `json:",embed"`
   198  			} `json:",embed"`
   199  			X map[string]jsontext.Value `json:",embed"`
   200  		}{},
   201  		want: structFields{
   202  			embeddedFallback: &structField{id: 0, index: []int{2}, typ: T[map[string]jsontext.Value](), fieldOptions: fieldOptions{name: "X", quotedName: `"X"`, embed: true}},
   203  		},
   204  	}, {
   205  		name: jsontest.Name("EmbeddedFallback/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("DuplicateName"),
   217  		in: struct {
   218  			A string `json:"same"`
   219  			B string `json:"same"`
   220  		}{},
   221  		want:    structFields{flattened: []structField{}},
   222  		wantErr: errors.New(`Go struct fields A and B conflict over JSON object name "same"`),
   223  	}, {
   224  		name: jsontest.Name("EmbedWithOptions"),
   225  		in: struct {
   226  			A struct{} `json:",embed,omitempty"`
   227  		}{},
   228  		wantErr: errors.New("Go struct field A cannot have any options other than `embed` specified"),
   229  	}, {
   230  		name: jsontest.Name("UnknownWithOptions"),
   231  		in: struct {
   232  			A map[string]any `json:",embed,omitempty"`
   233  		}{},
   234  		want: structFields{embeddedFallback: &structField{
   235  			index: []int{0},
   236  			typ:   reflect.TypeFor[map[string]any](),
   237  			fieldOptions: fieldOptions{
   238  				name:       "A",
   239  				quotedName: `"A"`,
   240  				embed:      true,
   241  			},
   242  		}},
   243  		wantErr: errors.New("Go struct field A cannot have any options other than `embed` specified"),
   244  	}, {
   245  		name: jsontest.Name("EmbedTextMarshaler"),
   246  		in: struct {
   247  			A struct{ encoding.TextMarshaler } `json:",embed"`
   248  		}{},
   249  		want: structFields{flattened: []structField{{
   250  			index: []int{0, 0},
   251  			typ:   reflect.TypeFor[encoding.TextMarshaler](),
   252  			fieldOptions: fieldOptions{
   253  				name:       "TextMarshaler",
   254  				quotedName: `"TextMarshaler"`,
   255  			},
   256  		}}},
   257  		wantErr: errors.New(`embedded Go struct field A of type struct { encoding.TextMarshaler } must not implement marshal or unmarshal methods`),
   258  	}, {
   259  		name: jsontest.Name("EmbedTextAppender"),
   260  		in: struct {
   261  			A struct{ encoding.TextAppender } `json:",embed"`
   262  		}{},
   263  		want: structFields{flattened: []structField{{
   264  			index: []int{0, 0},
   265  			typ:   reflect.TypeFor[encoding.TextAppender](),
   266  			fieldOptions: fieldOptions{
   267  				name:       "TextAppender",
   268  				quotedName: `"TextAppender"`,
   269  			},
   270  		}}},
   271  		wantErr: errors.New(`embedded Go struct field A of type struct { encoding.TextAppender } must not implement marshal or unmarshal methods`),
   272  	}, {
   273  		name: jsontest.Name("EmbedJSONMarshaler"),
   274  		in: struct {
   275  			A struct{ Marshaler } `json:",embed"`
   276  		}{},
   277  		want: structFields{flattened: []structField{{
   278  			index: []int{0, 0},
   279  			typ:   reflect.TypeFor[Marshaler](),
   280  			fieldOptions: fieldOptions{
   281  				name:       "Marshaler",
   282  				quotedName: `"Marshaler"`,
   283  			},
   284  		}}},
   285  		wantErr: errors.New(`embedded Go struct field A of type struct { json.Marshaler } must not implement marshal or unmarshal methods`),
   286  	}, {
   287  		name: jsontest.Name("EmbedJSONMarshalerTo"),
   288  		in: struct {
   289  			A struct{ MarshalerTo } `json:",embed"`
   290  		}{},
   291  		want: structFields{flattened: []structField{{
   292  			index: []int{0, 0},
   293  			typ:   reflect.TypeFor[MarshalerTo](),
   294  			fieldOptions: fieldOptions{
   295  				name:       "MarshalerTo",
   296  				quotedName: `"MarshalerTo"`,
   297  			},
   298  		}}},
   299  		wantErr: errors.New(`embedded Go struct field A of type struct { json.MarshalerTo } must not implement marshal or unmarshal methods`),
   300  	}, {
   301  		name: jsontest.Name("EmbedTextUnmarshaler"),
   302  		in: struct {
   303  			A *struct{ encoding.TextUnmarshaler } `json:",embed"`
   304  		}{},
   305  		want: structFields{flattened: []structField{{
   306  			index: []int{0, 0},
   307  			typ:   reflect.TypeFor[encoding.TextUnmarshaler](),
   308  			fieldOptions: fieldOptions{
   309  				name:       "TextUnmarshaler",
   310  				quotedName: `"TextUnmarshaler"`,
   311  			},
   312  		}}},
   313  		wantErr: errors.New(`embedded Go struct field A of type struct { encoding.TextUnmarshaler } must not implement marshal or unmarshal methods`),
   314  	}, {
   315  		name: jsontest.Name("EmbedJSONUnmarshaler"),
   316  		in: struct {
   317  			A *struct{ Unmarshaler } `json:",embed"`
   318  		}{},
   319  		want: structFields{flattened: []structField{{
   320  			index: []int{0, 0},
   321  			typ:   reflect.TypeFor[Unmarshaler](),
   322  			fieldOptions: fieldOptions{
   323  				name:       "Unmarshaler",
   324  				quotedName: `"Unmarshaler"`,
   325  			},
   326  		}}},
   327  		wantErr: errors.New(`embedded Go struct field A of type struct { json.Unmarshaler } must not implement marshal or unmarshal methods`),
   328  	}, {
   329  		name: jsontest.Name("EmbedJSONUnmarshalerFrom"),
   330  		in: struct {
   331  			A struct{ UnmarshalerFrom } `json:",embed"`
   332  		}{},
   333  		want: structFields{flattened: []structField{{
   334  			index: []int{0, 0},
   335  			typ:   reflect.TypeFor[UnmarshalerFrom](),
   336  			fieldOptions: fieldOptions{
   337  				name:       "UnmarshalerFrom",
   338  				quotedName: `"UnmarshalerFrom"`,
   339  			},
   340  		}}},
   341  		wantErr: errors.New(`embedded Go struct field A of type struct { json.UnmarshalerFrom } must not implement marshal or unmarshal methods`),
   342  	}, {
   343  		name: jsontest.Name("EmbedUnsupported/MapIntKey"),
   344  		in: struct {
   345  			A map[int]any `json:",embed"`
   346  		}{},
   347  		want: structFields{flattened: []structField{{
   348  			index:        []int{0},
   349  			typ:          reflect.TypeFor[map[int]any](),
   350  			fieldOptions: fieldOptions{name: "A", quotedName: `"A"`, embed: true},
   351  		}}},
   352  		wantErr: errors.New(`embedded Go struct field A of type map[int]interface {} must be a Go struct, Go map of string key, or jsontext.Value`),
   353  	}, {
   354  		name: jsontest.Name("EmbedUnsupported/MapTextMarshalerStringKey"),
   355  		in: struct {
   356  			A map[nocaseString]any `json:",embed"`
   357  		}{},
   358  		want: structFields{flattened: []structField{{
   359  			index:        []int{0},
   360  			typ:          reflect.TypeFor[map[nocaseString]any](),
   361  			fieldOptions: fieldOptions{name: "A", quotedName: `"A"`, embed: true},
   362  		}}},
   363  		wantErr: errors.New(`embedded map field A of type map[json.nocaseString]interface {} must have a string key that does not implement marshal or unmarshal methods`),
   364  	}, {
   365  		name: jsontest.Name("EmbedUnsupported/MapMarshalerStringKey"),
   366  		in: struct {
   367  			A map[stringMarshalEmpty]any `json:",embed"`
   368  		}{},
   369  		want: structFields{flattened: []structField{{
   370  			index:        []int{0},
   371  			typ:          reflect.TypeFor[map[stringMarshalEmpty]any](),
   372  			fieldOptions: fieldOptions{name: "A", quotedName: `"A"`, embed: true},
   373  		}}},
   374  		wantErr: errors.New(`embedded map field A of type map[json.stringMarshalEmpty]interface {} must have a string key that does not implement marshal or unmarshal methods`),
   375  	}, {
   376  		name: jsontest.Name("EmbedUnsupported/DoublePointer"),
   377  		in: struct {
   378  			A **struct{} `json:",embed"`
   379  		}{},
   380  		want: structFields{flattened: []structField{{
   381  			index:        []int{0},
   382  			typ:          reflect.TypeFor[**struct{}](),
   383  			fieldOptions: fieldOptions{name: "A", quotedName: `"A"`, embed: true},
   384  		}}},
   385  		wantErr: errors.New(`embedded Go struct field A of type *struct {} must be a Go struct, Go map of string key, or jsontext.Value`),
   386  	}, {
   387  		name: jsontest.Name("DuplicateEmbed"),
   388  		in: struct {
   389  			A map[string]any `json:",embed"`
   390  			B jsontext.Value `json:",embed"`
   391  		}{},
   392  		wantErr: errors.New(`embedded Go struct fields A and B cannot both be a Go map or jsontext.Value`),
   393  	}, {
   394  		name: jsontest.Name("DuplicateEmbedEmbed"),
   395  		in: struct {
   396  			A MapStringAny   `json:",embed"`
   397  			B jsontext.Value `json:",embed"`
   398  		}{},
   399  		wantErr: errors.New(`embedded Go struct fields A and B cannot both be a Go map or jsontext.Value`),
   400  	}}
   401  
   402  	for _, tt := range tests {
   403  		t.Run(tt.name.Name, func(t *testing.T) {
   404  			got, err := makeStructFields(reflect.TypeOf(tt.in))
   405  
   406  			// Sanity check that pointers are consistent.
   407  			pointers := make(map[*structField]bool)
   408  			for i := range got.flattened {
   409  				pointers[&got.flattened[i]] = true
   410  			}
   411  			for _, f := range got.byActualName {
   412  				if !pointers[f] {
   413  					t.Errorf("%s: byActualName pointer not in flattened", tt.name.Where)
   414  				}
   415  			}
   416  			for _, fs := range got.byFoldedName {
   417  				for _, f := range fs {
   418  					if !pointers[f] {
   419  						t.Errorf("%s: byFoldedName pointer not in flattened", tt.name.Where)
   420  					}
   421  				}
   422  			}
   423  
   424  			// Zero out fields that are incomparable.
   425  			for i := range got.flattened {
   426  				got.flattened[i].fncs = nil
   427  				got.flattened[i].isEmpty = nil
   428  			}
   429  			if got.embeddedFallback != nil {
   430  				got.embeddedFallback.fncs = nil
   431  				got.embeddedFallback.isEmpty = nil
   432  			}
   433  
   434  			// Reproduce maps in want.
   435  			tt.want.byActualName = make(map[string]*structField)
   436  			for i := range tt.want.flattened {
   437  				f := &tt.want.flattened[i]
   438  				tt.want.byActualName[f.name] = f
   439  			}
   440  			tt.want.byFoldedName = make(map[string][]*structField)
   441  			for i, f := range tt.want.flattened {
   442  				foldedName := string(foldName([]byte(f.name)))
   443  				tt.want.byFoldedName[foldedName] = append(tt.want.byFoldedName[foldedName], &tt.want.flattened[i])
   444  			}
   445  
   446  			// Only compare underlying error to simplify test logic.
   447  			var gotErr error
   448  			if err != nil {
   449  				gotErr = err.Err
   450  			}
   451  
   452  			tt.want.reindex()
   453  			if !reflect.DeepEqual(got, tt.want) || !reflect.DeepEqual(gotErr, tt.wantErr) {
   454  				t.Errorf("%s: makeStructFields(%T):\n\tgot  (%v, %v)\n\twant (%v, %v)", tt.name.Where, tt.in, got, gotErr, tt.want, tt.wantErr)
   455  			}
   456  		})
   457  	}
   458  }
   459  
   460  func TestParseTagOptions(t *testing.T) {
   461  	tests := []struct {
   462  		name        jsontest.CaseName
   463  		in          any // must be a struct with a single field
   464  		wantOpts    fieldOptions
   465  		wantIgnored bool
   466  		wantErr     error
   467  	}{{
   468  		name: jsontest.Name("GoName"),
   469  		in: struct {
   470  			FieldName int
   471  		}{},
   472  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
   473  	}, {
   474  		name: jsontest.Name("GoNameWithOptions"),
   475  		in: struct {
   476  			FieldName int `json:",embed"`
   477  		}{},
   478  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, embed: true},
   479  	}, {
   480  		name: jsontest.Name("Empty"),
   481  		in: struct {
   482  			V int `json:""`
   483  		}{},
   484  		wantOpts: fieldOptions{name: "V", quotedName: `"V"`},
   485  	}, {
   486  		name: jsontest.Name("Unexported"),
   487  		in: struct {
   488  			v int `json:"Hello"`
   489  		}{},
   490  		wantIgnored: true,
   491  		wantErr:     errors.New("unexported Go struct field v cannot have non-ignored `json:\"Hello\"` tag"),
   492  	}, {
   493  		name: jsontest.Name("UnexportedEmpty"),
   494  		in: struct {
   495  			v int `json:""`
   496  		}{},
   497  		wantIgnored: true,
   498  		wantErr:     errors.New("unexported Go struct field v cannot have non-ignored `json:\"\"` tag"),
   499  	}, {
   500  		name: jsontest.Name("EmbedUnexported"),
   501  		in: struct {
   502  			unexported
   503  		}{},
   504  		wantOpts: fieldOptions{name: "unexported", quotedName: `"unexported"`},
   505  	}, {
   506  		name: jsontest.Name("Ignored"),
   507  		in: struct {
   508  			V int `json:"-"`
   509  		}{},
   510  		wantIgnored: true,
   511  	}, {
   512  		name: jsontest.Name("IgnoredEmbedUnexported"),
   513  		in: struct {
   514  			unexported `json:"-"`
   515  		}{},
   516  		wantIgnored: true,
   517  	}, {
   518  		name: jsontest.Name("DashComma"),
   519  		in: struct {
   520  			V int `json:"-,"`
   521  		}{},
   522  		wantOpts: fieldOptions{hasName: true, name: "-", quotedName: `"-"`},
   523  		wantErr:  errors.New("Go struct field V has malformed `json` tag: invalid trailing ',' character"),
   524  	}, {
   525  		name: jsontest.Name("LatinPunctuationName"),
   526  		in: struct {
   527  			V int `json:"$%-/"`
   528  		}{},
   529  		wantOpts: fieldOptions{hasName: true, name: "$%-/", quotedName: `"$%-/"`},
   530  	}, {
   531  		name: jsontest.Name("LatinDigitsName"),
   532  		in: struct {
   533  			V int `json:"0123456789"`
   534  		}{},
   535  		wantOpts: fieldOptions{hasName: true, name: "0123456789", quotedName: `"0123456789"`},
   536  	}, {
   537  		name: jsontest.Name("LatinUppercaseName"),
   538  		in: struct {
   539  			V int `json:"ABCDEFGHIJKLMOPQRSTUVWXYZ"`
   540  		}{},
   541  		wantOpts: fieldOptions{hasName: true, name: "ABCDEFGHIJKLMOPQRSTUVWXYZ", quotedName: `"ABCDEFGHIJKLMOPQRSTUVWXYZ"`},
   542  	}, {
   543  		name: jsontest.Name("LatinLowercaseName"),
   544  		in: struct {
   545  			V int `json:"abcdefghijklmnopqrstuvwxyz_"`
   546  		}{},
   547  		wantOpts: fieldOptions{hasName: true, name: "abcdefghijklmnopqrstuvwxyz_", quotedName: `"abcdefghijklmnopqrstuvwxyz_"`},
   548  	}, {
   549  		name: jsontest.Name("GreekName"),
   550  		in: struct {
   551  			V string `json:"Ελλάδα"`
   552  		}{},
   553  		wantOpts: fieldOptions{hasName: true, name: "Ελλάδα", quotedName: `"Ελλάδα"`},
   554  	}, {
   555  		name: jsontest.Name("ChineseName"),
   556  		in: struct {
   557  			V string `json:"世界"`
   558  		}{},
   559  		wantOpts: fieldOptions{hasName: true, name: "世界", quotedName: `"世界"`},
   560  	}, {
   561  		name: jsontest.Name("PercentSlashName"),
   562  		in: struct {
   563  			V int `json:"text/html%"`
   564  		}{},
   565  		wantOpts: fieldOptions{hasName: true, name: "text/html%", quotedName: `"text/html%"`},
   566  	}, {
   567  		name: jsontest.Name("PunctuationName"),
   568  		in: struct {
   569  			V string `json:"!#$%&()*+-./:;<=>?@[]^_{|}~ "`
   570  		}{},
   571  		wantOpts: fieldOptions{hasName: true, name: "!#$%&()*+-./:;<=>?@[]^_{|}~ ", quotedName: `"!#$%&()*+-./:;<=>?@[]^_{|}~ "`, nameNeedEscape: true},
   572  	}, {
   573  		name: jsontest.Name("SingleComma"),
   574  		in: struct {
   575  			V int `json:","`
   576  		}{},
   577  		wantOpts: fieldOptions{name: "V", quotedName: `"V"`},
   578  		wantErr:  errors.New("Go struct field V has malformed `json` tag: invalid trailing ',' character"),
   579  	}, {
   580  		name: jsontest.Name("SuperfluousCommas"),
   581  		in: struct {
   582  			V int `json:",,,,\"\",,embed,,,,,"`
   583  		}{},
   584  		wantOpts: fieldOptions{name: "V", quotedName: `"V"`, embed: true},
   585  		wantErr:  errors.New("Go struct field V has malformed `json` tag: invalid character ',' at start of option (expecting Unicode letter)"),
   586  	}, {
   587  		name: jsontest.Name("CaseAloneOption"),
   588  		in: struct {
   589  			FieldName int `json:",case"`
   590  		}{},
   591  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
   592  		wantErr:  errors.New("Go struct field FieldName is missing value for `case` tag option; specify `case:ignore` or `case:strict` instead"),
   593  	}, {
   594  		name: jsontest.Name("CaseIgnoreOption"),
   595  		in: struct {
   596  			FieldName int `json:",case:ignore"`
   597  		}{},
   598  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, casing: caseIgnore},
   599  	}, {
   600  		name: jsontest.Name("CaseStrictOption"),
   601  		in: struct {
   602  			FieldName int `json:",case:strict"`
   603  		}{},
   604  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, casing: caseStrict},
   605  	}, {
   606  		name: jsontest.Name("CaseUnknownOption"),
   607  		in: struct {
   608  			FieldName int `json:",case:unknown"`
   609  		}{},
   610  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
   611  		wantErr:  errors.New("Go struct field FieldName has unknown `case:unknown` tag value"),
   612  	}, {
   613  		name: jsontest.Name("CaseQuotedOption"),
   614  		in: struct {
   615  			FieldName int `json:",case:'ignore'"`
   616  		}{},
   617  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
   618  		wantErr:  errors.New("Go struct field FieldName has malformed value for `case` tag option: invalid character '\\'' at start of option (expecting Unicode letter)"),
   619  	}, {
   620  		name: jsontest.Name("BothCaseOptions"),
   621  		in: struct {
   622  			FieldName int `json:",case:ignore,case:strict"`
   623  		}{},
   624  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, casing: caseIgnore | caseStrict},
   625  		wantErr:  errors.New("Go struct field FieldName cannot have both `case:ignore` and `case:strict` tag options"),
   626  	}, {
   627  		name: jsontest.Name("EmbedOption"),
   628  		in: struct {
   629  			FieldName int `json:",embed"`
   630  		}{},
   631  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, embed: true},
   632  	}, {
   633  		name: jsontest.Name("OmitZeroOption"),
   634  		in: struct {
   635  			FieldName int `json:",omitzero"`
   636  		}{},
   637  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, omitzero: true},
   638  	}, {
   639  		name: jsontest.Name("OmitEmptyOption"),
   640  		in: struct {
   641  			FieldName int `json:",omitempty"`
   642  		}{},
   643  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, omitempty: true},
   644  	}, {
   645  		name: jsontest.Name("StringOption"),
   646  		in: struct {
   647  			FieldName int `json:",string"`
   648  		}{},
   649  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, string: true},
   650  	}, {
   651  		name: jsontest.Name("FormatOptionEqual"),
   652  		in: struct {
   653  			FieldName int `json:",format=fizzbuzz"`
   654  		}{},
   655  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
   656  		wantErr:  errors.New("Go struct field FieldName is missing value for `format` tag option"),
   657  	}, {
   658  		name: jsontest.Name("FormatOptionColon"),
   659  		in: struct {
   660  			FieldName int `json:",format:fizzbuzz"`
   661  		}{},
   662  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, format: "fizzbuzz"},
   663  	}, {
   664  		name: jsontest.Name("FormatOptionQuoted"),
   665  		in: struct {
   666  			FieldName int `json:",format:'2006-01-02'"`
   667  		}{},
   668  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, format: "2006-01-02"},
   669  	}, {
   670  		name: jsontest.Name("FormatOptionInvalid"),
   671  		in: struct {
   672  			FieldName int `json:",format:'2006-01-02"`
   673  		}{},
   674  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
   675  		wantErr:  errors.New("Go struct field FieldName has malformed value for `format` tag option: single-quoted string not terminated: '2006-01-0..."),
   676  	}, {
   677  		name: jsontest.Name("FormatOptionNotLast"),
   678  		in: struct {
   679  			FieldName int `json:",format:alpha,ordered"`
   680  		}{},
   681  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, format: "alpha"},
   682  		wantErr:  errors.New("Go struct field FieldName has `format` tag option that was not specified last"),
   683  	}, {
   684  		name: jsontest.Name("AllOptions"),
   685  		in: struct {
   686  			FieldName int `json:",case:ignore,embed,omitzero,omitempty,string,format:format"`
   687  		}{},
   688  		wantOpts: fieldOptions{
   689  			name:       "FieldName",
   690  			quotedName: `"FieldName"`,
   691  			casing:     caseIgnore,
   692  			embed:      true,
   693  			omitzero:   true,
   694  			omitempty:  true,
   695  			string:     true,
   696  			format:     "format",
   697  		},
   698  	}, {
   699  		name: jsontest.Name("AllOptionsCaseSensitive"),
   700  		in: struct {
   701  			FieldName int `json:",CASE:IGNORE,INLINE,UNKNOWN,OMITZERO,OMITEMPTY,STRING,FORMAT:FORMAT"`
   702  		}{},
   703  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
   704  		wantErr:  errors.New("Go struct field FieldName has invalid appearance of `CASE` tag option; specify `case` instead"),
   705  	}, {
   706  		name: jsontest.Name("AllOptionsSpaceSensitive"),
   707  		in: struct {
   708  			FieldName int `json:", case:ignore , embed , omitzero , omitempty , string , format:format "`
   709  		}{},
   710  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
   711  		wantErr:  errors.New("Go struct field FieldName has malformed `json` tag: invalid character ' ' at start of option (expecting Unicode letter)"),
   712  	}, {
   713  		name: jsontest.Name("UnknownTagOption"),
   714  		in: struct {
   715  			FieldName int `json:",embed,whoknows,string"`
   716  		}{},
   717  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, embed: true, string: true},
   718  	}, {
   719  		name: jsontest.Name("MisnamedTag"),
   720  		in: struct {
   721  			V int `jsom:"Misnamed"`
   722  		}{},
   723  		wantOpts: fieldOptions{name: "V", quotedName: `"V"`},
   724  	}}
   725  
   726  	for _, tt := range tests {
   727  		t.Run(tt.name.Name, func(t *testing.T) {
   728  			fs := reflect.TypeOf(tt.in).Field(0)
   729  			gotOpts, gotIgnored, gotErr := parseFieldOptions(fs)
   730  			if !reflect.DeepEqual(gotOpts, tt.wantOpts) || gotIgnored != tt.wantIgnored || !reflect.DeepEqual(gotErr, tt.wantErr) {
   731  				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)
   732  			}
   733  		})
   734  	}
   735  }
   736  

View as plain text