k8s.io/kube-openapi@v0.0.0-20240228011516-70dd3763d340/pkg/internal/third_party/go-json-experiment/json/fields_test.go (about)

     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  package json
     6  
     7  import (
     8  	"encoding"
     9  	"errors"
    10  	"reflect"
    11  	"testing"
    12  )
    13  
    14  type unexported struct{}
    15  
    16  func TestMakeStructFields(t *testing.T) {
    17  	type Embed struct {
    18  		Foo string
    19  	}
    20  	type Recursive struct {
    21  		A          string
    22  		*Recursive `json:",inline"`
    23  		B          string
    24  	}
    25  	type MapStringAny map[string]any
    26  	tests := []struct {
    27  		name    testName
    28  		in      any
    29  		want    structFields
    30  		wantErr error
    31  	}{{
    32  		name: name("Names"),
    33  		in: struct {
    34  			F1 string
    35  			F2 string `json:"-"`
    36  			F3 string `json:"json_name"`
    37  			f3 string
    38  			F5 string `json:"json_name_nocase,nocase"`
    39  		}{},
    40  		want: structFields{
    41  			flattened: []structField{
    42  				{id: 0, index: []int{0}, typ: stringType, fieldOptions: fieldOptions{name: "F1", quotedName: `"F1"`}},
    43  				{id: 1, index: []int{2}, typ: stringType, fieldOptions: fieldOptions{name: "json_name", quotedName: `"json_name"`, hasName: true}},
    44  				{id: 2, index: []int{4}, typ: stringType, fieldOptions: fieldOptions{name: "json_name_nocase", quotedName: `"json_name_nocase"`, hasName: true, nocase: true}},
    45  			},
    46  		},
    47  	}, {
    48  		name: name("BreadthFirstSearch"),
    49  		in: struct {
    50  			L1A string
    51  			L1B struct {
    52  				L2A string
    53  				L2B struct {
    54  					L3A string
    55  				} `json:",inline"`
    56  				L2C string
    57  			} `json:",inline"`
    58  			L1C string
    59  			L1D struct {
    60  				L2D string
    61  				L2E struct {
    62  					L3B string
    63  				} `json:",inline"`
    64  				L2F string
    65  			} `json:",inline"`
    66  			L1E string
    67  		}{},
    68  		want: structFields{
    69  			flattened: []structField{
    70  				{id: 0, index: []int{0}, typ: stringType, fieldOptions: fieldOptions{name: "L1A", quotedName: `"L1A"`}},
    71  				{id: 3, index: []int{1, 0}, typ: stringType, fieldOptions: fieldOptions{name: "L2A", quotedName: `"L2A"`}},
    72  				{id: 7, index: []int{1, 1, 0}, typ: stringType, fieldOptions: fieldOptions{name: "L3A", quotedName: `"L3A"`}},
    73  				{id: 4, index: []int{1, 2}, typ: stringType, fieldOptions: fieldOptions{name: "L2C", quotedName: `"L2C"`}},
    74  				{id: 1, index: []int{2}, typ: stringType, fieldOptions: fieldOptions{name: "L1C", quotedName: `"L1C"`}},
    75  				{id: 5, index: []int{3, 0}, typ: stringType, fieldOptions: fieldOptions{name: "L2D", quotedName: `"L2D"`}},
    76  				{id: 8, index: []int{3, 1, 0}, typ: stringType, fieldOptions: fieldOptions{name: "L3B", quotedName: `"L3B"`}},
    77  				{id: 6, index: []int{3, 2}, typ: stringType, fieldOptions: fieldOptions{name: "L2F", quotedName: `"L2F"`}},
    78  				{id: 2, index: []int{4}, typ: stringType, fieldOptions: fieldOptions{name: "L1E", quotedName: `"L1E"`}},
    79  			},
    80  		},
    81  	}, {
    82  		name: name("NameResolution"),
    83  		in: struct {
    84  			X1 struct {
    85  				X struct {
    86  					A string // loses in precedence to A
    87  					B string // cancels out with X2.X.B
    88  					D string // loses in precedence to D
    89  				} `json:",inline"`
    90  			} `json:",inline"`
    91  			X2 struct {
    92  				X struct {
    93  					B string // cancels out with X1.X.B
    94  					C string
    95  					D string // loses in precedence to D
    96  				} `json:",inline"`
    97  			} `json:",inline"`
    98  			A string // takes precedence over X1.X.A
    99  			D string // takes precedence over X1.X.D and X2.X.D
   100  		}{},
   101  		want: structFields{
   102  			flattened: []structField{
   103  				{id: 2, index: []int{1, 0, 1}, typ: stringType, fieldOptions: fieldOptions{name: "C", quotedName: `"C"`}},
   104  				{id: 0, index: []int{2}, typ: stringType, fieldOptions: fieldOptions{name: "A", quotedName: `"A"`}},
   105  				{id: 1, index: []int{3}, typ: stringType, fieldOptions: fieldOptions{name: "D", quotedName: `"D"`}},
   106  			},
   107  		},
   108  	}, {
   109  		name: name("Embed/Implicit"),
   110  		in: struct {
   111  			Embed
   112  		}{},
   113  		want: structFields{
   114  			flattened: []structField{
   115  				{id: 0, index: []int{0, 0}, typ: stringType, fieldOptions: fieldOptions{name: "Foo", quotedName: `"Foo"`}},
   116  			},
   117  		},
   118  	}, {
   119  		name: name("Embed/Explicit"),
   120  		in: struct {
   121  			Embed `json:",inline"`
   122  		}{},
   123  		want: structFields{
   124  			flattened: []structField{
   125  				{id: 0, index: []int{0, 0}, typ: stringType, fieldOptions: fieldOptions{name: "Foo", quotedName: `"Foo"`}},
   126  			},
   127  		},
   128  	}, {
   129  		name: name("Recursive"),
   130  		in: struct {
   131  			A         string
   132  			Recursive `json:",inline"`
   133  			C         string
   134  		}{},
   135  		want: structFields{
   136  			flattened: []structField{
   137  				{id: 0, index: []int{0}, typ: stringType, fieldOptions: fieldOptions{name: "A", quotedName: `"A"`}},
   138  				{id: 2, index: []int{1, 2}, typ: stringType, fieldOptions: fieldOptions{name: "B", quotedName: `"B"`}},
   139  				{id: 1, index: []int{2}, typ: stringType, fieldOptions: fieldOptions{name: "C", quotedName: `"C"`}},
   140  			},
   141  		},
   142  	}, {
   143  		name: name("InlinedFallback/Cancelation"),
   144  		in: struct {
   145  			X1 struct {
   146  				X RawValue `json:",inline"`
   147  			} `json:",inline"`
   148  			X2 struct {
   149  				X map[string]any `json:",unknown"`
   150  			} `json:",inline"`
   151  		}{},
   152  		want: structFields{},
   153  	}, {
   154  		name: name("InlinedFallback/Precedence"),
   155  		in: struct {
   156  			X1 struct {
   157  				X RawValue `json:",inline"`
   158  			} `json:",inline"`
   159  			X2 struct {
   160  				X map[string]any `json:",unknown"`
   161  			} `json:",inline"`
   162  			X map[string]RawValue `json:",unknown"`
   163  		}{},
   164  		want: structFields{
   165  			inlinedFallback: &structField{id: 0, index: []int{2}, typ: reflect.TypeOf(map[string]RawValue(nil)), fieldOptions: fieldOptions{name: "X", quotedName: `"X"`, unknown: true}},
   166  		},
   167  	}, {
   168  		name: name("InvalidUTF8"),
   169  		in: struct {
   170  			Name string `json:"'\\xde\\xad\\xbe\\xef'"`
   171  		}{},
   172  		wantErr: errors.New(`Go struct field Name has JSON object name "ޭ\xbe\xef" with invalid UTF-8`),
   173  	}, {
   174  		name: name("DuplicateName"),
   175  		in: struct {
   176  			A string `json:"same"`
   177  			B string `json:"same"`
   178  		}{},
   179  		wantErr: errors.New(`Go struct fields A and B conflict over JSON object name "same"`),
   180  	}, {
   181  		name: name("BothInlineAndUnknown"),
   182  		in: struct {
   183  			A struct{} `json:",inline,unknown"`
   184  		}{},
   185  		wantErr: errors.New("Go struct field A cannot have both `inline` and `unknown` specified"),
   186  	}, {
   187  		name: name("InlineWithOptions"),
   188  		in: struct {
   189  			A struct{} `json:",inline,omitempty"`
   190  		}{},
   191  		wantErr: errors.New("Go struct field A cannot have any options other than `inline` or `unknown` specified"),
   192  	}, {
   193  		name: name("UnknownWithOptions"),
   194  		in: struct {
   195  			A map[string]any `json:",inline,omitempty"`
   196  		}{},
   197  		wantErr: errors.New("Go struct field A cannot have any options other than `inline` or `unknown` specified"),
   198  	}, {
   199  		name: name("InlineTextMarshaler"),
   200  		in: struct {
   201  			A struct{ encoding.TextMarshaler } `json:",inline"`
   202  		}{},
   203  		wantErr: errors.New(`inlined Go struct field A of type struct { encoding.TextMarshaler } must not implement JSON marshal or unmarshal methods`),
   204  	}, {
   205  		name: name("UnknownJSONMarshalerV1"),
   206  		in: struct {
   207  			A struct{ MarshalerV1 } `json:",unknown"`
   208  		}{},
   209  		wantErr: errors.New(`inlined Go struct field A of type struct { json.MarshalerV1 } must not implement JSON marshal or unmarshal methods`),
   210  	}, {
   211  		name: name("InlineJSONMarshalerV2"),
   212  		in: struct {
   213  			A struct{ MarshalerV2 } `json:",inline"`
   214  		}{},
   215  		wantErr: errors.New(`inlined Go struct field A of type struct { json.MarshalerV2 } must not implement JSON marshal or unmarshal methods`),
   216  	}, {
   217  		name: name("UnknownTextUnmarshaler"),
   218  		in: struct {
   219  			A *struct{ encoding.TextUnmarshaler } `json:",unknown"`
   220  		}{},
   221  		wantErr: errors.New(`inlined Go struct field A of type struct { encoding.TextUnmarshaler } must not implement JSON marshal or unmarshal methods`),
   222  	}, {
   223  		name: name("InlineJSONUnmarshalerV1"),
   224  		in: struct {
   225  			A *struct{ UnmarshalerV1 } `json:",inline"`
   226  		}{},
   227  		wantErr: errors.New(`inlined Go struct field A of type struct { json.UnmarshalerV1 } must not implement JSON marshal or unmarshal methods`),
   228  	}, {
   229  		name: name("UnknownJSONUnmarshalerV2"),
   230  		in: struct {
   231  			A struct{ UnmarshalerV2 } `json:",unknown"`
   232  		}{},
   233  		wantErr: errors.New(`inlined Go struct field A of type struct { json.UnmarshalerV2 } must not implement JSON marshal or unmarshal methods`),
   234  	}, {
   235  		name: name("UnknownStruct"),
   236  		in: struct {
   237  			A struct {
   238  				X, Y, Z string
   239  			} `json:",unknown"`
   240  		}{},
   241  		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 json.RawValue"),
   242  	}, {
   243  		name: name("InlineUnsupported/MapIntKey"),
   244  		in: struct {
   245  			A map[int]any `json:",unknown"`
   246  		}{},
   247  		wantErr: errors.New(`inlined Go struct field A of type map[int]interface {} must be a Go struct, Go map of string key, or json.RawValue`),
   248  	}, {
   249  		name: name("InlineUnsupported/MapNamedStringKey"),
   250  		in: struct {
   251  			A map[namedString]any `json:",inline"`
   252  		}{},
   253  		wantErr: errors.New(`inlined Go struct field A of type map[json.namedString]interface {} must be a Go struct, Go map of string key, or json.RawValue`),
   254  	}, {
   255  		name: name("InlineUnsupported/DoublePointer"),
   256  		in: struct {
   257  			A **struct{} `json:",inline"`
   258  		}{},
   259  		wantErr: errors.New(`inlined Go struct field A of type *struct {} must be a Go struct, Go map of string key, or json.RawValue`),
   260  	}, {
   261  		name: name("DuplicateInline"),
   262  		in: struct {
   263  			A map[string]any `json:",inline"`
   264  			B RawValue       `json:",inline"`
   265  		}{},
   266  		wantErr: errors.New(`inlined Go struct fields A and B cannot both be a Go map or json.RawValue`),
   267  	}, {
   268  		name: name("DuplicateEmbedInline"),
   269  		in: struct {
   270  			MapStringAny
   271  			B RawValue `json:",inline"`
   272  		}{},
   273  		wantErr: errors.New(`inlined Go struct fields MapStringAny and B cannot both be a Go map or json.RawValue`),
   274  	}}
   275  
   276  	for _, tt := range tests {
   277  		t.Run(tt.name.name, func(t *testing.T) {
   278  			got, err := makeStructFields(reflect.TypeOf(tt.in))
   279  
   280  			// Sanity check that pointers are consistent.
   281  			pointers := make(map[*structField]bool)
   282  			for i := range got.flattened {
   283  				pointers[&got.flattened[i]] = true
   284  			}
   285  			for _, f := range got.byActualName {
   286  				if !pointers[f] {
   287  					t.Errorf("%s: byActualName pointer not in flattened", tt.name.where)
   288  				}
   289  			}
   290  			for _, fs := range got.byFoldedName {
   291  				for _, f := range fs {
   292  					if !pointers[f] {
   293  						t.Errorf("%s: byFoldedName pointer not in flattened", tt.name.where)
   294  					}
   295  				}
   296  			}
   297  
   298  			// Zero out fields that are incomparable.
   299  			for i := range got.flattened {
   300  				got.flattened[i].fncs = nil
   301  				got.flattened[i].isEmpty = nil
   302  			}
   303  			if got.inlinedFallback != nil {
   304  				got.inlinedFallback.fncs = nil
   305  				got.inlinedFallback.isEmpty = nil
   306  			}
   307  
   308  			// Reproduce maps in want.
   309  			if tt.wantErr == nil {
   310  				tt.want.byActualName = make(map[string]*structField)
   311  				for i := range tt.want.flattened {
   312  					f := &tt.want.flattened[i]
   313  					tt.want.byActualName[f.name] = f
   314  				}
   315  				tt.want.byFoldedName = make(map[string][]*structField)
   316  				for i, f := range tt.want.flattened {
   317  					foldedName := string(foldName([]byte(f.name)))
   318  					tt.want.byFoldedName[foldedName] = append(tt.want.byFoldedName[foldedName], &tt.want.flattened[i])
   319  				}
   320  			}
   321  
   322  			// Only compare underlying error to simplify test logic.
   323  			var gotErr error
   324  			if err != nil {
   325  				gotErr = err.Err
   326  			}
   327  
   328  			if !reflect.DeepEqual(got, tt.want) || !reflect.DeepEqual(gotErr, tt.wantErr) {
   329  				t.Errorf("%s: makeStructFields(%T):\n\tgot  (%v, %v)\n\twant (%v, %v)", tt.name.where, tt.in, got, gotErr, tt.want, tt.wantErr)
   330  			}
   331  		})
   332  	}
   333  }
   334  
   335  func TestParseTagOptions(t *testing.T) {
   336  	tests := []struct {
   337  		name     testName
   338  		in       any // must be a struct with a single field
   339  		wantOpts fieldOptions
   340  		wantErr  error
   341  	}{{
   342  		name: name("GoName"),
   343  		in: struct {
   344  			FieldName int
   345  		}{},
   346  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
   347  	}, {
   348  		name: name("GoNameWithOptions"),
   349  		in: struct {
   350  			FieldName int `json:",inline"`
   351  		}{},
   352  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, inline: true},
   353  	}, {
   354  		name: name("Empty"),
   355  		in: struct {
   356  			V int `json:""`
   357  		}{},
   358  		wantOpts: fieldOptions{name: "V", quotedName: `"V"`},
   359  	}, {
   360  		name: name("Unexported"),
   361  		in: struct {
   362  			v int `json:"Hello"`
   363  		}{},
   364  		wantErr: errors.New("unexported Go struct field v cannot have non-ignored `json:\"Hello\"` tag"),
   365  	}, {
   366  		name: name("UnexportedEmpty"),
   367  		in: struct {
   368  			v int `json:""`
   369  		}{},
   370  		wantErr: errors.New("unexported Go struct field v cannot have non-ignored `json:\"\"` tag"),
   371  	}, {
   372  		name: name("EmbedUnexported"),
   373  		in: struct {
   374  			unexported
   375  		}{},
   376  		wantErr: errors.New("embedded Go struct field unexported of an unexported type must be explicitly ignored with a `json:\"-\"` tag"),
   377  	}, {
   378  		name: name("Ignored"),
   379  		in: struct {
   380  			V int `json:"-"`
   381  		}{},
   382  		wantErr: errIgnoredField,
   383  	}, {
   384  		name: name("IgnoredEmbedUnexported"),
   385  		in: struct {
   386  			unexported `json:"-"`
   387  		}{},
   388  		wantErr: errIgnoredField,
   389  	}, {
   390  		name: name("DashComma"),
   391  		in: struct {
   392  			V int `json:"-,"`
   393  		}{},
   394  		wantErr: errors.New("Go struct field V has malformed `json` tag: invalid trailing ',' character"),
   395  	}, {
   396  		name: name("QuotedDashName"),
   397  		in: struct {
   398  			V int `json:"'-'"`
   399  		}{},
   400  		wantOpts: fieldOptions{hasName: true, name: "-", quotedName: `"-"`},
   401  	}, {
   402  		name: name("LatinPunctuationName"),
   403  		in: struct {
   404  			V int `json:"$%-/"`
   405  		}{},
   406  		wantOpts: fieldOptions{hasName: true, name: "$%-/", quotedName: `"$%-/"`},
   407  	}, {
   408  		name: name("QuotedLatinPunctuationName"),
   409  		in: struct {
   410  			V int `json:"'$%-/'"`
   411  		}{},
   412  		wantOpts: fieldOptions{hasName: true, name: "$%-/", quotedName: `"$%-/"`},
   413  	}, {
   414  		name: name("LatinDigitsName"),
   415  		in: struct {
   416  			V int `json:"0123456789"`
   417  		}{},
   418  		wantOpts: fieldOptions{hasName: true, name: "0123456789", quotedName: `"0123456789"`},
   419  	}, {
   420  		name: name("QuotedLatinDigitsName"),
   421  		in: struct {
   422  			V int `json:"'0123456789'"`
   423  		}{},
   424  		wantOpts: fieldOptions{hasName: true, name: "0123456789", quotedName: `"0123456789"`},
   425  	}, {
   426  		name: name("LatinUppercaseName"),
   427  		in: struct {
   428  			V int `json:"ABCDEFGHIJKLMOPQRSTUVWXYZ"`
   429  		}{},
   430  		wantOpts: fieldOptions{hasName: true, name: "ABCDEFGHIJKLMOPQRSTUVWXYZ", quotedName: `"ABCDEFGHIJKLMOPQRSTUVWXYZ"`},
   431  	}, {
   432  		name: name("LatinLowercaseName"),
   433  		in: struct {
   434  			V int `json:"abcdefghijklmnopqrstuvwxyz_"`
   435  		}{},
   436  		wantOpts: fieldOptions{hasName: true, name: "abcdefghijklmnopqrstuvwxyz_", quotedName: `"abcdefghijklmnopqrstuvwxyz_"`},
   437  	}, {
   438  		name: name("GreekName"),
   439  		in: struct {
   440  			V string `json:"Ελλάδα"`
   441  		}{},
   442  		wantOpts: fieldOptions{hasName: true, name: "Ελλάδα", quotedName: `"Ελλάδα"`},
   443  	}, {
   444  		name: name("QuotedGreekName"),
   445  		in: struct {
   446  			V string `json:"'Ελλάδα'"`
   447  		}{},
   448  		wantOpts: fieldOptions{hasName: true, name: "Ελλάδα", quotedName: `"Ελλάδα"`},
   449  	}, {
   450  		name: name("ChineseName"),
   451  		in: struct {
   452  			V string `json:"世界"`
   453  		}{},
   454  		wantOpts: fieldOptions{hasName: true, name: "世界", quotedName: `"世界"`},
   455  	}, {
   456  		name: name("QuotedChineseName"),
   457  		in: struct {
   458  			V string `json:"'世界'"`
   459  		}{},
   460  		wantOpts: fieldOptions{hasName: true, name: "世界", quotedName: `"世界"`},
   461  	}, {
   462  		name: name("PercentSlashName"),
   463  		in: struct {
   464  			V int `json:"text/html%"`
   465  		}{},
   466  		wantOpts: fieldOptions{hasName: true, name: "text/html%", quotedName: `"text/html%"`},
   467  	}, {
   468  		name: name("QuotedPercentSlashName"),
   469  		in: struct {
   470  			V int `json:"'text/html%'"`
   471  		}{},
   472  		wantOpts: fieldOptions{hasName: true, name: "text/html%", quotedName: `"text/html%"`},
   473  	}, {
   474  		name: name("PunctuationName"),
   475  		in: struct {
   476  			V string `json:"!#$%&()*+-./:;<=>?@[]^_{|}~ "`
   477  		}{},
   478  		wantOpts: fieldOptions{hasName: true, name: "!#$%&()*+-./:;<=>?@[]^_{|}~ ", quotedName: `"!#$%&()*+-./:;<=>?@[]^_{|}~ "`},
   479  	}, {
   480  		name: name("QuotedPunctuationName"),
   481  		in: struct {
   482  			V string `json:"'!#$%&()*+-./:;<=>?@[]^_{|}~ '"`
   483  		}{},
   484  		wantOpts: fieldOptions{hasName: true, name: "!#$%&()*+-./:;<=>?@[]^_{|}~ ", quotedName: `"!#$%&()*+-./:;<=>?@[]^_{|}~ "`},
   485  	}, {
   486  		name: name("EmptyName"),
   487  		in: struct {
   488  			V int `json:"''"`
   489  		}{},
   490  		wantOpts: fieldOptions{hasName: true, name: "", quotedName: `""`},
   491  	}, {
   492  		name: name("SpaceName"),
   493  		in: struct {
   494  			V int `json:"' '"`
   495  		}{},
   496  		wantOpts: fieldOptions{hasName: true, name: " ", quotedName: `" "`},
   497  	}, {
   498  		name: name("CommaQuotes"),
   499  		in: struct {
   500  			V int `json:"',\\'\"\\\"'"`
   501  		}{},
   502  		wantOpts: fieldOptions{hasName: true, name: `,'""`, quotedName: `",'\"\""`},
   503  	}, {
   504  		name: name("SingleComma"),
   505  		in: struct {
   506  			V int `json:","`
   507  		}{},
   508  		wantErr: errors.New("Go struct field V has malformed `json` tag: invalid trailing ',' character"),
   509  	}, {
   510  		name: name("SuperfluousCommas"),
   511  		in: struct {
   512  			V int `json:",,,,\"\",,inline,unknown,,,,"`
   513  		}{},
   514  		wantErr: errors.New("Go struct field V has malformed `json` tag: invalid character ',' at start of option (expecting Unicode letter or single quote)"),
   515  	}, {
   516  		name: name("NoCaseOption"),
   517  		in: struct {
   518  			FieldName int `json:",nocase"`
   519  		}{},
   520  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, nocase: true},
   521  	}, {
   522  		name: name("InlineOption"),
   523  		in: struct {
   524  			FieldName int `json:",inline"`
   525  		}{},
   526  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, inline: true},
   527  	}, {
   528  		name: name("UnknownOption"),
   529  		in: struct {
   530  			FieldName int `json:",unknown"`
   531  		}{},
   532  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, unknown: true},
   533  	}, {
   534  		name: name("OmitZeroOption"),
   535  		in: struct {
   536  			FieldName int `json:",omitzero"`
   537  		}{},
   538  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, omitzero: true},
   539  	}, {
   540  		name: name("OmitEmptyOption"),
   541  		in: struct {
   542  			FieldName int `json:",omitempty"`
   543  		}{},
   544  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, omitempty: true},
   545  	}, {
   546  		name: name("StringOption"),
   547  		in: struct {
   548  			FieldName int `json:",string"`
   549  		}{},
   550  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, string: true},
   551  	}, {
   552  		name: name("FormatOptionEqual"),
   553  		in: struct {
   554  			FieldName int `json:",format=fizzbuzz"`
   555  		}{},
   556  		wantErr: errors.New("Go struct field FieldName is missing value for `format` tag option"),
   557  	}, {
   558  		name: name("FormatOptionColon"),
   559  		in: struct {
   560  			FieldName int `json:",format:fizzbuzz"`
   561  		}{},
   562  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, format: "fizzbuzz"},
   563  	}, {
   564  		name: name("FormatOptionQuoted"),
   565  		in: struct {
   566  			FieldName int `json:",format:'2006-01-02'"`
   567  		}{},
   568  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, format: "2006-01-02"},
   569  	}, {
   570  		name: name("FormatOptionInvalid"),
   571  		in: struct {
   572  			FieldName int `json:",format:'2006-01-02"`
   573  		}{},
   574  		wantErr: errors.New("Go struct field FieldName has malformed value for `format` tag option: single-quoted string not terminated: '2006-01-0..."),
   575  	}, {
   576  		name: name("FormatOptionNotLast"),
   577  		in: struct {
   578  			FieldName int `json:",format:alpha,ordered"`
   579  		}{},
   580  		wantErr: errors.New("Go struct field FieldName has `format` tag option that was not specified last"),
   581  	}, {
   582  		name: name("AllOptions"),
   583  		in: struct {
   584  			FieldName int `json:",nocase,inline,unknown,omitzero,omitempty,string,format:format"`
   585  		}{},
   586  		wantOpts: fieldOptions{
   587  			name:       "FieldName",
   588  			quotedName: `"FieldName"`,
   589  			nocase:     true,
   590  			inline:     true,
   591  			unknown:    true,
   592  			omitzero:   true,
   593  			omitempty:  true,
   594  			string:     true,
   595  			format:     "format",
   596  		},
   597  	}, {
   598  		name: name("AllOptionsQuoted"),
   599  		in: struct {
   600  			FieldName int `json:",'nocase','inline','unknown','omitzero','omitempty','string','format':'format'"`
   601  		}{},
   602  		wantErr: errors.New("Go struct field FieldName has unnecessarily quoted appearance of `'nocase'` tag option; specify `nocase` instead"),
   603  	}, {
   604  		name: name("AllOptionsCaseSensitive"),
   605  		in: struct {
   606  			FieldName int `json:",NOCASE,INLINE,UNKNOWN,OMITZERO,OMITEMPTY,STRING,FORMAT:FORMAT"`
   607  		}{},
   608  		wantErr: errors.New("Go struct field FieldName has invalid appearance of `NOCASE` tag option; specify `nocase` instead"),
   609  	}, {
   610  		name: name("AllOptionsSpaceSensitive"),
   611  		in: struct {
   612  			FieldName int `json:", nocase , inline , unknown , omitzero , omitempty , string , format:format "`
   613  		}{},
   614  		wantErr: errors.New("Go struct field FieldName has malformed `json` tag: invalid character ' ' at start of option (expecting Unicode letter or single quote)"),
   615  	}, {
   616  		name: name("UnknownTagOption"),
   617  		in: struct {
   618  			FieldName int `json:",inline,whoknows,string"`
   619  		}{},
   620  		wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, inline: true, string: true},
   621  	}, {
   622  		name: name("MalformedQuotedString/MissingQuote"),
   623  		in: struct {
   624  			FieldName int `json:"'hello,string"`
   625  		}{},
   626  		wantErr: errors.New("Go struct field FieldName has malformed `json` tag: single-quoted string not terminated: 'hello,str..."),
   627  	}, {
   628  		name: name("MalformedQuotedString/MissingComma"),
   629  		in: struct {
   630  			FieldName int `json:"'hello'inline,string"`
   631  		}{},
   632  		wantErr: errors.New("Go struct field FieldName has malformed `json` tag: invalid character 'i' before next option (expecting ',')"),
   633  	}, {
   634  		name: name("MalformedQuotedString/InvalidEscape"),
   635  		in: struct {
   636  			FieldName int `json:"'hello\\u####',inline,string"`
   637  		}{},
   638  		wantErr: errors.New("Go struct field FieldName has malformed `json` tag: invalid single-quoted string: 'hello\\u####'"),
   639  	}, {
   640  		name: name("MisnamedTag"),
   641  		in: struct {
   642  			V int `jsom:"Misnamed"`
   643  		}{},
   644  		wantOpts: fieldOptions{name: "V", quotedName: `"V"`},
   645  	}}
   646  
   647  	for _, tt := range tests {
   648  		t.Run(tt.name.name, func(t *testing.T) {
   649  			fs := reflect.TypeOf(tt.in).Field(0)
   650  			gotOpts, gotErr := parseFieldOptions(fs)
   651  			if !reflect.DeepEqual(gotOpts, tt.wantOpts) || !reflect.DeepEqual(gotErr, tt.wantErr) {
   652  				t.Errorf("%s: parseFieldOptions(%T) = (%v, %v), want (%v, %v)", tt.name.where, tt.in, gotOpts, gotErr, tt.wantOpts, tt.wantErr)
   653  			}
   654  		})
   655  	}
   656  }