From 6510be0a694ddd0b79d509294686675ab1cee4b4 Mon Sep 17 00:00:00 2001 From: elv-gilles Date: Mon, 8 Apr 2024 13:26:17 +0200 Subject: [PATCH] allow skip in UnmarshalJSONV2 methods * fix skipping in arshal_test.go --- arshal_methods.go | 19 ++ arshal_test.go | 270 +++++++++++++++++- config_arshal_vs_func/config_1_enc_json.go | 129 +++++++++ config_arshal_vs_func/config_2_unmarshalV2.go | 157 ++++++++++ .../config_3_unmarshalFunc.go | 128 +++++++++ .../config_4_unmarshal_skip.go | 147 ++++++++++ config_arshal_vs_func/config_test.go | 119 ++++++++ 7 files changed, 959 insertions(+), 10 deletions(-) create mode 100644 config_arshal_vs_func/config_1_enc_json.go create mode 100644 config_arshal_vs_func/config_2_unmarshalV2.go create mode 100644 config_arshal_vs_func/config_3_unmarshalFunc.go create mode 100644 config_arshal_vs_func/config_4_unmarshal_skip.go create mode 100644 config_arshal_vs_func/config_test.go diff --git a/arshal_methods.go b/arshal_methods.go index f7277c9..3e60a6f 100644 --- a/arshal_methods.go +++ b/arshal_methods.go @@ -110,6 +110,7 @@ func makeMethodArshaler(fncs *arshaler, t reflect.Type) *arshaler { switch which := implementsWhich(t, jsonMarshalerV2Type, jsonMarshalerV1Type, textAppenderType, textMarshalerType); which { case jsonMarshalerV2Type: fncs.nonDefault = true + defMarshal := fncs.marshal fncs.marshal = func(enc *jsontext.Encoder, va addressableValue, mo *jsonopts.Struct) error { xe := export.Encoder(enc) prevDepth, prevLength := xe.Tokens.DepthLength() @@ -120,6 +121,15 @@ func makeMethodArshaler(fncs *arshaler, t reflect.Type) *arshaler { if (prevDepth != currDepth || prevLength+1 != currLength) && err == nil { err = errors.New("must write exactly one JSON value") } + if err == SkipFunc && defMarshal != nil { + if prevDepth == currDepth && prevLength == currLength { + fncs.nonDefault = false + err = defMarshal(enc, va, mo) + } else { + err = errors.New("must not write any JSON tokens when skipping") + } + } + if err != nil { err = wrapSkipFunc(err, "marshal method") // TODO: Avoid wrapping semantic or I/O errors. @@ -188,6 +198,7 @@ func makeMethodArshaler(fncs *arshaler, t reflect.Type) *arshaler { switch which := implementsWhich(t, jsonUnmarshalerV2Type, jsonUnmarshalerV1Type, textUnmarshalerType); which { case jsonUnmarshalerV2Type: fncs.nonDefault = true + defUnmarshal := fncs.unmarshal fncs.unmarshal = func(dec *jsontext.Decoder, va addressableValue, uo *jsonopts.Struct) error { xd := export.Decoder(dec) prevDepth, prevLength := xd.Tokens.DepthLength() @@ -198,6 +209,14 @@ func makeMethodArshaler(fncs *arshaler, t reflect.Type) *arshaler { if (prevDepth != currDepth || prevLength+1 != currLength) && err == nil { err = errors.New("must read exactly one JSON value") } + if err == SkipFunc && defUnmarshal != nil { + if prevDepth == currDepth && prevLength == currLength { + fncs.nonDefault = false + err = defUnmarshal(dec, va, uo) + } else { + err = errors.New("must not read any JSON tokens when skipping") + } + } if err != nil { err = wrapSkipFunc(err, "unmarshal method") // TODO: Avoid wrapping semantic, syntactic, or I/O errors. diff --git a/arshal_test.go b/arshal_test.go index 5a30073..56d2375 100644 --- a/arshal_test.go +++ b/arshal_test.go @@ -129,6 +129,7 @@ type ( Pointer *structAll Interface any } + structAllSkip structAll structStringifiedAll struct { Bool bool `json:",string"` String string `json:",string"` @@ -481,6 +482,13 @@ type ( } ) +func (s *structAllSkip) UnmarshalJSONV2(dec *jsontext.Decoder, opts Options) error { + return SkipFunc +} +func (s *structAllSkip) MarshalJSONV2(*jsontext.Encoder, Options) error { + return SkipFunc +} + func (p *allMethods) MarshalJSONV2(enc *jsontext.Encoder, opts Options) error { if got, want := "MarshalJSONV2", p.method; got != want { return fmt.Errorf("called wrong method: got %v, want %v", got, want) @@ -3041,10 +3049,150 @@ func TestMarshal(t *testing.T) { wantErr: &SemanticError{action: "marshal", GoType: marshalJSONv2FuncType, Err: errors.New("must write exactly one JSON value")}, }, { name: jsontest.Name("Methods/Invalid/JSONv2/SkipFunc"), - in: marshalJSONv2Func(func(enc *jsontext.Encoder, opts Options) error { - return SkipFunc - }), - wantErr: &SemanticError{action: "marshal", GoType: marshalJSONv2FuncType, Err: errors.New("marshal method cannot be skipped")}, + opts: []Options{jsontext.Expand(true)}, + in: structAllSkip{ + Bool: true, + String: "hello", + Bytes: []byte{1, 2, 3}, + Int: -64, + Uint: +64, + Float: 3.14159, + Map: map[string]string{"key": "value"}, + StructScalars: structScalars{ + Bool: true, + String: "hello", + Bytes: []byte{1, 2, 3}, + Int: -64, + Uint: +64, + Float: 3.14159, + }, + StructMaps: structMaps{ + MapBool: map[string]bool{"": true}, + MapString: map[string]string{"": "hello"}, + MapBytes: map[string][]byte{"": {1, 2, 3}}, + MapInt: map[string]int64{"": -64}, + MapUint: map[string]uint64{"": +64}, + MapFloat: map[string]float64{"": 3.14159}, + }, + StructSlices: structSlices{ + SliceBool: []bool{true}, + SliceString: []string{"hello"}, + SliceBytes: [][]byte{{1, 2, 3}}, + SliceInt: []int64{-64}, + SliceUint: []uint64{+64}, + SliceFloat: []float64{3.14159}, + }, + Slice: []string{"fizz", "buzz"}, + Array: [1]string{"goodbye"}, + Pointer: new(structAll), + Interface: (*structAll)(nil), + }, + want: `{ + "Bool": true, + "String": "hello", + "Bytes": "AQID", + "Int": -64, + "Uint": 64, + "Float": 3.14159, + "Map": { + "key": "value" + }, + "StructScalars": { + "Bool": true, + "String": "hello", + "Bytes": "AQID", + "Int": -64, + "Uint": 64, + "Float": 3.14159 + }, + "StructMaps": { + "MapBool": { + "": true + }, + "MapString": { + "": "hello" + }, + "MapBytes": { + "": "AQID" + }, + "MapInt": { + "": -64 + }, + "MapUint": { + "": 64 + }, + "MapFloat": { + "": 3.14159 + } + }, + "StructSlices": { + "SliceBool": [ + true + ], + "SliceString": [ + "hello" + ], + "SliceBytes": [ + "AQID" + ], + "SliceInt": [ + -64 + ], + "SliceUint": [ + 64 + ], + "SliceFloat": [ + 3.14159 + ] + }, + "Slice": [ + "fizz", + "buzz" + ], + "Array": [ + "goodbye" + ], + "Pointer": { + "Bool": false, + "String": "", + "Bytes": "", + "Int": 0, + "Uint": 0, + "Float": 0, + "Map": {}, + "StructScalars": { + "Bool": false, + "String": "", + "Bytes": "", + "Int": 0, + "Uint": 0, + "Float": 0 + }, + "StructMaps": { + "MapBool": {}, + "MapString": {}, + "MapBytes": {}, + "MapInt": {}, + "MapUint": {}, + "MapFloat": {} + }, + "StructSlices": { + "SliceBool": [], + "SliceString": [], + "SliceBytes": [], + "SliceInt": [], + "SliceUint": [], + "SliceFloat": [] + }, + "Slice": [], + "Array": [ + "" + ], + "Pointer": null, + "Interface": null + }, + "Interface": null +}`, }, { name: jsontest.Name("Methods/Invalid/JSONv1/Error"), in: marshalJSONv1Func(func() ([]byte, error) { @@ -7126,12 +7274,114 @@ func TestUnmarshal(t *testing.T) { })), wantErr: &SemanticError{action: "unmarshal", GoType: unmarshalJSONv2FuncType, Err: errors.New("must read exactly one JSON value")}, }, { - name: jsontest.Name("Methods/Invalid/JSONv2/SkipFunc"), - inBuf: `{}`, - inVal: addr(unmarshalJSONv2Func(func(*jsontext.Decoder, Options) error { - return SkipFunc - })), - wantErr: &SemanticError{action: "unmarshal", GoType: unmarshalJSONv2FuncType, Err: errors.New("unmarshal method cannot be skipped")}, + name: jsontest.Name("Methods/Invalid/JSONv2/SkipFunc"), + inBuf: `{ + "Bool": true, + "String": "hello", + "Bytes": "AQID", + "Int": -64, + "Uint": 64, + "Float": 3.14159, + "Map": { + "key": "value" + }, + "StructScalars": { + "Bool": true, + "String": "hello", + "Bytes": "AQID", + "Int": -64, + "Uint": 64, + "Float": 3.14159 + }, + "StructMaps": { + "MapBool": { + "": true + }, + "MapString": { + "": "hello" + }, + "MapBytes": { + "": "AQID" + }, + "MapInt": { + "": -64 + }, + "MapUint": { + "": 64 + }, + "MapFloat": { + "": 3.14159 + } + }, + "StructSlices": { + "SliceBool": [ + true + ], + "SliceString": [ + "hello" + ], + "SliceBytes": [ + "AQID" + ], + "SliceInt": [ + -64 + ], + "SliceUint": [ + 64 + ], + "SliceFloat": [ + 3.14159 + ] + }, + "Slice": [ + "fizz", + "buzz" + ], + "Array": [ + "goodbye" + ], + "Pointer": null, + "Interface": null +}`, + inVal: new(structAllSkip), + opts: []Options{}, + want: &structAllSkip{ + Bool: true, + String: "hello", + Bytes: []byte{1, 2, 3}, + Int: -64, + Uint: +64, + Float: 3.14159, + Map: map[string]string{"key": "value"}, + StructScalars: structScalars{ + Bool: true, + String: "hello", + Bytes: []byte{1, 2, 3}, + Int: -64, + Uint: +64, + Float: 3.14159, + }, + StructMaps: structMaps{ + MapBool: map[string]bool{"": true}, + MapString: map[string]string{"": "hello"}, + MapBytes: map[string][]byte{"": {1, 2, 3}}, + MapInt: map[string]int64{"": -64}, + MapUint: map[string]uint64{"": +64}, + MapFloat: map[string]float64{"": 3.14159}, + }, + StructSlices: structSlices{ + SliceBool: []bool{true}, + SliceString: []string{"hello"}, + SliceBytes: [][]byte{{1, 2, 3}}, + SliceInt: []int64{-64}, + SliceUint: []uint64{+64}, + SliceFloat: []float64{3.14159}, + }, + Slice: []string{"fizz", "buzz"}, + Array: [1]string{"goodbye"}, + Pointer: nil, + }, + wantErr: nil, }, { name: jsontest.Name("Methods/Invalid/JSONv1/Error"), inBuf: `{}`, diff --git a/config_arshal_vs_func/config_1_enc_json.go b/config_arshal_vs_func/config_1_enc_json.go new file mode 100644 index 0000000..f6cf8c0 --- /dev/null +++ b/config_arshal_vs_func/config_1_enc_json.go @@ -0,0 +1,129 @@ +package config_arshal_vs_func + +import ( + "encoding/json" + "fmt" +) + +type AppConfig1 struct { + QSpacesConfig1 +} + +// InitDefaults initializes the default configuration. +func (c *AppConfig1) InitDefaults() *AppConfig1 { + c.QSpacesConfig1.InitDefaults() + return c +} + +func (c *AppConfig1) UnmarshalJSON(bts []byte) error { + c.InitDefaults() + + type alias AppConfig1 + return json.Unmarshal(bts, (*alias)(c)) +} + +type QSpacesConfig1 struct { + QSpaces []QSpaceConfig1 `json:"qspaces"` +} + +func (c *QSpacesConfig1) Validate() error { + names := make(map[string]bool) + for _, sp := range c.QSpaces { + if len(sp.Names) > 0 { + for _, n := range sp.Names { + _, ok := names[n] + if ok { + return fmt.Errorf("duplicate space name %s", n) + } + names[n] = true + } + } + if err := sp.Validate(); err != nil { + return err + } + } + return nil +} + +func (c *QSpacesConfig1) InitDefaults() *QSpacesConfig1 { + c.QSpaces = make([]QSpaceConfig1, 1) + c.QSpaces[0] = QSpaceConfig1{ + ID: "ispcMockContentSpace", + Type: "Mock", + Mock: (&MockQSpaceConfig1{}).InitDefaults(), + } + c.QSpaces[0].Services.InitDefaults() + return c +} + +type QSpaceConfig1 struct { + ID string `json:"id"` // space ID + Names []string `json:"names"` // space names + Type string `json:"type"` // Type is one of the supported content space types + Mock *MockQSpaceConfig1 `json:"mock,omitempty"` // Mock is the configuration for a mock content space + Ethereum *EthereumQSpaceConfig1 `json:"ethereum,omitempty"` + Services SpaceServicesConfig1 `json:"services"` +} + +func (c *QSpaceConfig1) InitDefaults() *QSpaceConfig1 { + c.Services.InitDefaults() + return c +} + +func (c *QSpaceConfig1) Validate() error { + if c.Type == "Ethereum" { + if c.Ethereum == nil { + return fmt.Errorf("ethereum not defined") + } + return c.Ethereum.Validate() + } + return nil +} + +func (c *QSpaceConfig1) UnmarshalJSON(bts []byte) error { + c.InitDefaults() + + type alias QSpaceConfig1 + return json.Unmarshal(bts, (*alias)(c)) +} + +type SpaceServicesConfig1 struct{} + +func (c *SpaceServicesConfig1) InitDefaults() *SpaceServicesConfig1 { + return c +} + +type MockQSpaceConfig1 struct { + TokenAuth bool `json:"token_auth"` +} + +func (c *MockQSpaceConfig1) InitDefaults() *MockQSpaceConfig1 { + return c +} + +type EthereumQSpaceConfig1 struct { + NetworkId uint64 `json:"network_id,omitempty"` // network ID + URL string `json:"url,omitempty"` // URL to the blockchain + WalletFile string `json:"wallet_file"` // wallet file path +} + +func (c *EthereumQSpaceConfig1) InitDefaults() *EthereumQSpaceConfig1 { + c.NetworkId = 123 + c.WalletFile = "/predefined" + return c +} + +func (c *EthereumQSpaceConfig1) Validate() error { + return nil +} + +func (c *EthereumQSpaceConfig1) UnmarshalJSON(p []byte) error { + c.InitDefaults() + + type alias EthereumQSpaceConfig1 + err := json.Unmarshal(p, (*alias)(c)) + if err == nil { + //c.Bc.NetworkId = c.NetworkId + } + return err +} diff --git a/config_arshal_vs_func/config_2_unmarshalV2.go b/config_arshal_vs_func/config_2_unmarshalV2.go new file mode 100644 index 0000000..2ac7288 --- /dev/null +++ b/config_arshal_vs_func/config_2_unmarshalV2.go @@ -0,0 +1,157 @@ +package config_arshal_vs_func + +import ( + "encoding/json" + "fmt" + + jsonexp "github.com/go-json-experiment/json" + "github.com/go-json-experiment/json/jsontext" +) + +type AppConfig2 struct { + QSpacesConfig2 +} + +// InitDefaults initializes the default configuration. +func (c *AppConfig2) InitDefaults() *AppConfig2 { + c.QSpacesConfig2.InitDefaults() + return c +} + +func (c *AppConfig2) UnmarshalJSON(bts []byte) error { + c.InitDefaults() + + type alias AppConfig2 + return json.Unmarshal(bts, (*alias)(c)) +} + +func (c *AppConfig2) UnmarshalJSONV2(dec *jsontext.Decoder, opts jsonexp.Options) error { + c.InitDefaults() + + type alias AppConfig2 + return jsonexp.UnmarshalDecode(dec, (*alias)(c), opts) +} + +type QSpacesConfig2 struct { + QSpaces []QSpaceConfig2 `json:"qspaces"` +} + +func (c *QSpacesConfig2) Validate() error { + names := make(map[string]bool) + for _, sp := range c.QSpaces { + if len(sp.Names) > 0 { + for _, n := range sp.Names { + _, ok := names[n] + if ok { + return fmt.Errorf("duplicate space name %s", n) + } + names[n] = true + } + } + if err := sp.Validate(); err != nil { + return err + } + } + return nil +} + +func (c *QSpacesConfig2) InitDefaults() *QSpacesConfig2 { + c.QSpaces = make([]QSpaceConfig2, 1) + c.QSpaces[0] = QSpaceConfig2{ + ID: "ispcMockContentSpace", + Type: "Mock", + Mock: (&MockQSpaceConfig2{}).InitDefaults(), + } + c.QSpaces[0].Services.InitDefaults() + return c +} + +type QSpaceConfig2 struct { + ID string `json:"id"` // space ID + Names []string `json:"names"` // space names + Type string `json:"type"` // Type is one of the supported content space types + Mock *MockQSpaceConfig2 `json:"mock,omitempty"` // Mock is the configuration for a mock content space + Ethereum *EthereumQSpaceConfig2 `json:"ethereum,omitempty"` + Services SpaceServicesConfig2 `json:"services"` +} + +func (c *QSpaceConfig2) InitDefaults() *QSpaceConfig2 { + c.Services.InitDefaults() + return c +} + +func (c *QSpaceConfig2) Validate() error { + if c.Type == "Ethereum" { + if c.Ethereum == nil { + return fmt.Errorf("ethereum not defined") + } + return c.Ethereum.Validate() + } + return nil +} + +func (c *QSpaceConfig2) UnmarshalJSON(bts []byte) error { + c.InitDefaults() + + type alias QSpaceConfig2 + return json.Unmarshal(bts, (*alias)(c)) +} + +func (c *QSpaceConfig2) UnmarshalJSONV2(dec *jsontext.Decoder, opts jsonexp.Options) error { + c.InitDefaults() + + type alias QSpaceConfig2 + return jsonexp.UnmarshalDecode(dec, (*alias)(c), opts) +} + +type SpaceServicesConfig2 struct{} + +func (c *SpaceServicesConfig2) InitDefaults() *SpaceServicesConfig2 { + return c +} + +type MockQSpaceConfig2 struct { + TokenAuth bool `json:"token_auth"` +} + +func (c *MockQSpaceConfig2) InitDefaults() *MockQSpaceConfig2 { + return c +} + +type EthereumQSpaceConfig2 struct { + NetworkId uint64 `json:"network_id,omitempty"` // network ID + URL string `json:"url,omitempty"` // URL to the blockchain + WalletFile string `json:"wallet_file"` // wallet file path +} + +func (c *EthereumQSpaceConfig2) InitDefaults() *EthereumQSpaceConfig2 { + c.NetworkId = 123 + c.WalletFile = "/predefined" + return c +} + +func (c *EthereumQSpaceConfig2) Validate() error { + return nil +} + +func (c *EthereumQSpaceConfig2) UnmarshalJSON(p []byte) error { + c.InitDefaults() + + type alias EthereumQSpaceConfig2 + err := json.Unmarshal(p, (*alias)(c)) + if err == nil { + //c.Bc.NetworkId = c.NetworkId + } + return err +} + +func (c *EthereumQSpaceConfig2) UnmarshalJSONV2(dec *jsontext.Decoder, opts jsonexp.Options) error { + c.InitDefaults() + + type alias EthereumQSpaceConfig2 + err := jsonexp.UnmarshalDecode(dec, (*alias)(c), opts) + if err == nil { + //c.Bc.NetworkId = c.NetworkId + } + return err +} diff --git a/config_arshal_vs_func/config_3_unmarshalFunc.go b/config_arshal_vs_func/config_3_unmarshalFunc.go new file mode 100644 index 0000000..1b3b9e0 --- /dev/null +++ b/config_arshal_vs_func/config_3_unmarshalFunc.go @@ -0,0 +1,128 @@ +package config_arshal_vs_func + +import ( + "fmt" +) + +type AppConfig3 struct { + QSpacesConfig3 +} + +// InitDefaults initializes the default configuration. +func (c *AppConfig3) InitDefaults() *AppConfig3 { + c.QSpacesConfig3.InitDefaults() + return c +} + +//func (c *AppConfig3) UnmarshalJSON(bts []byte) error { +// c.InitDefaults() +// +// type alias AppConfig3 +// return json.Unmarshal(bts, (*alias)(c)) +//} + +type QSpacesConfig3 struct { + QSpaces []QSpaceConfig3 `json:"qspaces"` +} + +func (c *QSpacesConfig3) Validate() error { + names := make(map[string]bool) + for _, sp := range c.QSpaces { + if len(sp.Names) > 0 { + for _, n := range sp.Names { + _, ok := names[n] + if ok { + return fmt.Errorf("duplicate space name %s", n) + } + names[n] = true + } + } + if err := sp.Validate(); err != nil { + return err + } + } + return nil +} + +func (c *QSpacesConfig3) InitDefaults() *QSpacesConfig3 { + c.QSpaces = make([]QSpaceConfig3, 1) + c.QSpaces[0] = QSpaceConfig3{ + ID: "ispcMockContentSpace", + Type: "Mock", + Mock: (&MockQSpaceConfig3{}).InitDefaults(), + } + c.QSpaces[0].Services.InitDefaults() + return c +} + +type QSpaceConfig3 struct { + ID string `json:"id"` // space ID + Names []string `json:"names"` // space names + Type string `json:"type"` // Type is one of the supported content space types + Mock *MockQSpaceConfig3 `json:"mock,omitempty"` // Mock is the configuration for a mock content space + Ethereum *EthereumQSpaceConfig3 `json:"ethereum,omitempty"` + Services SpaceServicesConfig3 `json:"services"` +} + +func (c *QSpaceConfig3) InitDefaults() *QSpaceConfig3 { + c.Services.InitDefaults() + return c +} + +func (c *QSpaceConfig3) Validate() error { + if c.Type == "Ethereum" { + if c.Ethereum == nil { + return fmt.Errorf("ethereum not defined") + } + return c.Ethereum.Validate() + } + return nil +} + +//func (c *QSpaceConfig3) UnmarshalJSON(bts []byte) error { +// c.InitDefaults() +// +// type alias QSpaceConfig3 +// return json.Unmarshal(bts, (*alias)(c)) +//} + +type SpaceServicesConfig3 struct{} + +func (c *SpaceServicesConfig3) InitDefaults() *SpaceServicesConfig3 { + return c +} + +type MockQSpaceConfig3 struct { + TokenAuth bool `json:"token_auth"` +} + +func (c *MockQSpaceConfig3) InitDefaults() *MockQSpaceConfig3 { + return c +} + +type EthereumQSpaceConfig3 struct { + NetworkId uint64 `json:"network_id,omitempty"` // network ID + URL string `json:"url,omitempty"` // URL to the blockchain + WalletFile string `json:"wallet_file"` // wallet file path +} + +func (c *EthereumQSpaceConfig3) InitDefaults() *EthereumQSpaceConfig3 { + c.NetworkId = 123 + c.WalletFile = "/predefined" + return c +} + +func (c *EthereumQSpaceConfig3) Validate() error { + return nil +} + +//func (c *EthereumQSpaceConfig3) UnmarshalJSON(p []byte) error { +// c.InitDefaults() +// +// type alias EthereumQSpaceConfig3 +// err := json.Unmarshal(p, (*alias)(c)) +// if err == nil { +// //c.Bc.NetworkId = c.NetworkId +// } +// return err +//} diff --git a/config_arshal_vs_func/config_4_unmarshal_skip.go b/config_arshal_vs_func/config_4_unmarshal_skip.go new file mode 100644 index 0000000..edcbf33 --- /dev/null +++ b/config_arshal_vs_func/config_4_unmarshal_skip.go @@ -0,0 +1,147 @@ +package config_arshal_vs_func + +import ( + "encoding/json" + "fmt" + + jsonexp "github.com/go-json-experiment/json" + "github.com/go-json-experiment/json/jsontext" +) + +type AppConfig4 struct { + QSpacesConfig4 +} + +// InitDefaults initializes the default configuration. +func (c *AppConfig4) InitDefaults() *AppConfig4 { + c.QSpacesConfig4.InitDefaults() + return c +} + +func (c *AppConfig4) UnmarshalJSON(bts []byte) error { + c.InitDefaults() + + type alias AppConfig4 + return json.Unmarshal(bts, (*alias)(c)) +} + +func (c *AppConfig4) UnmarshalJSONV2(dec *jsontext.Decoder, opts jsonexp.Options) error { + c.InitDefaults() + return jsonexp.SkipFunc +} + +type QSpacesConfig4 struct { + QSpaces []QSpaceConfig4 `json:"qspaces"` +} + +func (c *QSpacesConfig4) Validate() error { + names := make(map[string]bool) + for _, sp := range c.QSpaces { + if len(sp.Names) > 0 { + for _, n := range sp.Names { + _, ok := names[n] + if ok { + return fmt.Errorf("duplicate space name %s", n) + } + names[n] = true + } + } + if err := sp.Validate(); err != nil { + return err + } + } + return nil +} + +func (c *QSpacesConfig4) InitDefaults() *QSpacesConfig4 { + c.QSpaces = make([]QSpaceConfig4, 1) + c.QSpaces[0] = QSpaceConfig4{ + ID: "ispcMockContentSpace", + Type: "Mock", + Mock: (&MockQSpaceConfig4{}).InitDefaults(), + } + c.QSpaces[0].Services.InitDefaults() + return c +} + +type QSpaceConfig4 struct { + ID string `json:"id"` // space ID + Names []string `json:"names"` // space names + Type string `json:"type"` // Type is one of the supported content space types + Mock *MockQSpaceConfig4 `json:"mock,omitempty"` // Mock is the configuration for a mock content space + Ethereum *EthereumQSpaceConfig4 `json:"ethereum,omitempty"` + Services SpaceServicesConfig4 `json:"services"` +} + +func (c *QSpaceConfig4) InitDefaults() *QSpaceConfig4 { + c.Services.InitDefaults() + return c +} + +func (c *QSpaceConfig4) Validate() error { + if c.Type == "Ethereum" { + if c.Ethereum == nil { + return fmt.Errorf("ethereum not defined") + } + return c.Ethereum.Validate() + } + return nil +} + +func (c *QSpaceConfig4) UnmarshalJSON(bts []byte) error { + c.InitDefaults() + + type alias QSpaceConfig4 + return json.Unmarshal(bts, (*alias)(c)) +} + +func (c *QSpaceConfig4) UnmarshalJSONV2(dec *jsontext.Decoder, opts jsonexp.Options) error { + c.InitDefaults() + return jsonexp.SkipFunc +} + +type SpaceServicesConfig4 struct{} + +func (c *SpaceServicesConfig4) InitDefaults() *SpaceServicesConfig4 { + return c +} + +type MockQSpaceConfig4 struct { + TokenAuth bool `json:"token_auth"` +} + +func (c *MockQSpaceConfig4) InitDefaults() *MockQSpaceConfig4 { + return c +} + +type EthereumQSpaceConfig4 struct { + NetworkId uint64 `json:"network_id,omitempty"` // network ID + URL string `json:"url,omitempty"` // URL to the blockchain + WalletFile string `json:"wallet_file"` // wallet file path +} + +func (c *EthereumQSpaceConfig4) InitDefaults() *EthereumQSpaceConfig4 { + c.NetworkId = 123 + c.WalletFile = "/predefined" + return c +} + +func (c *EthereumQSpaceConfig4) Validate() error { + return nil +} + +func (c *EthereumQSpaceConfig4) UnmarshalJSON(p []byte) error { + c.InitDefaults() + + type alias EthereumQSpaceConfig4 + err := json.Unmarshal(p, (*alias)(c)) + if err == nil { + //c.Bc.NetworkId = c.NetworkId + } + return err +} + +func (c *EthereumQSpaceConfig4) UnmarshalJSONV2(dec *jsontext.Decoder, opts jsonexp.Options) error { + c.InitDefaults() + return jsonexp.SkipFunc +} diff --git a/config_arshal_vs_func/config_test.go b/config_arshal_vs_func/config_test.go new file mode 100644 index 0000000..67014f0 --- /dev/null +++ b/config_arshal_vs_func/config_test.go @@ -0,0 +1,119 @@ +package config_arshal_vs_func + +import ( + "bytes" + "fmt" + "testing" + + jsonexp "github.com/go-json-experiment/json" + "github.com/go-json-experiment/json/jsontext" + "github.com/stretchr/testify/require" +) + +var json1 = `{"qspaces":[{"id":"ispc123","names":[],"type":"Ethereum","ethereum":{"url":"slu://a/b"}}]}` + +// TestUnmarshalJson unmarshal with encoding/json +func TestUnmarshalJson(t *testing.T) { + c := &AppConfig1{} + err := jsonexp.Unmarshal([]byte(json1), c) + require.NoError(t, err) + + bb, err := jsonexp.Marshal(c) + require.NoError(t, err) + fmt.Println(string(bb)) + require.Equal(t, "ispc123", c.QSpaces[0].ID) + require.Equal(t, "Ethereum", c.QSpaces[0].Type) + require.NotNil(t, c.QSpaces[0].Ethereum) + require.Equal(t, uint64(123), c.QSpaces[0].Ethereum.NetworkId) + require.Equal(t, "slu://a/b", c.QSpaces[0].Ethereum.URL) + require.Equal(t, "/predefined", c.QSpaces[0].Ethereum.WalletFile) + require.NotNil(t, c.QSpaces[0].Mock) +} + +// TestUnmarshalerV2 tests with UnmarshalerV2 +func TestUnmarshalerV2(t *testing.T) { + c := &AppConfig2{} + err := jsonexp.Unmarshal([]byte(json1), c) + require.NoError(t, err) + + bb, err := jsonexp.Marshal(c) + require.NoError(t, err) + fmt.Println(string(bb)) + require.Equal(t, "ispc123", c.QSpaces[0].ID) + require.Equal(t, "Ethereum", c.QSpaces[0].Type) + require.NotNil(t, c.QSpaces[0].Ethereum) + require.Equal(t, uint64(123), c.QSpaces[0].Ethereum.NetworkId) + require.Equal(t, "slu://a/b", c.QSpaces[0].Ethereum.URL) + require.Equal(t, "/predefined", c.QSpaces[0].Ethereum.WalletFile) + // 'mock' is now nil (different from encoding/json) + require.Nil(t, c.QSpaces[0].Mock) +} + +// TestUnmarshalFuncV2 tests with UnmarshalFuncV2 +func TestUnmarshalFuncV2(t *testing.T) { + c := &AppConfig3{} + + dec := jsontext.NewDecoder(bytes.NewReader([]byte(json1))) + err := jsonexp.UnmarshalDecode(dec, c, + jsonexp.WithUnmarshalers( + jsonexp.UnmarshalFuncV2(func(dec *jsontext.Decoder, val any, opts jsonexp.Options) error { + switch x := val.(type) { + case *AppConfig3: + fmt.Println("app config") + x.InitDefaults() + case *QSpacesConfig3: + fmt.Println("qspace config") + x.InitDefaults() + case *EthereumQSpaceConfig3: + fmt.Println("ethereum config") + x.InitDefaults() + } + return jsonexp.SkipFunc + }))) + require.NoError(t, err) + + bb, err := jsonexp.Marshal(c) + require.NoError(t, err) + fmt.Println(string(bb)) + require.Equal(t, "ispc123", c.QSpaces[0].ID) + require.Equal(t, "Ethereum", c.QSpaces[0].Type) + require.NotNil(t, c.QSpaces[0].Ethereum) + require.Equal(t, uint64(123), c.QSpaces[0].Ethereum.NetworkId) + require.Equal(t, "slu://a/b", c.QSpaces[0].Ethereum.URL) + require.Equal(t, "/predefined", c.QSpaces[0].Ethereum.WalletFile) + require.Nil(t, c.QSpaces[0].Mock) +} + +// TestUnmarshalV2Skip tests that returning SkipFunc works with UnmarshalerV2 +func TestUnmarshalV2Skip(t *testing.T) { + c := &AppConfig4{} + err := jsonexp.Unmarshal([]byte(json1), c) + require.NoError(t, err) + + bb, err := jsonexp.Marshal(c) + require.NoError(t, err) + fmt.Println(string(bb)) + require.Equal(t, "ispc123", c.QSpaces[0].ID) + require.Equal(t, "Ethereum", c.QSpaces[0].Type) + require.NotNil(t, c.QSpaces[0].Ethereum) + require.Equal(t, uint64(123), c.QSpaces[0].Ethereum.NetworkId) + require.Equal(t, "slu://a/b", c.QSpaces[0].Ethereum.URL) + require.Equal(t, "/predefined", c.QSpaces[0].Ethereum.WalletFile) + require.Nil(t, c.QSpaces[0].Mock) +} + +/* +=== RUN TestUnmarshalJson +{"qspaces":[{"id":"ispc123","names":[],"type":"Ethereum","mock":{"token_auth":false},"ethereum":{"network_id":123,"url":"slu://a/b","wallet_file":"/predefined"},"services":{}}]} + +=== RUN TestUnmarshalerV2 +{"qspaces":[{"id":"ispc123","names":[],"type":"Ethereum","ethereum":{"network_id":123,"url":"slu://a/b","wallet_file":"/predefined"},"services":{}}]} + +=== RUN TestUnmarshalFuncV2 +{"qspaces":[{"id":"ispc123","names":[],"type":"Ethereum","mock":{"token_auth":false},"ethereum":{"network_id":123,"url":"slu://a/b","wallet_file":"/predefined"},"services":{}}]} + +=== RUN TestUnmarshalV2Skip +{"qspaces":[{"id":"ispc123","names":[],"type":"Ethereum","ethereum":{"network_id":123,"url":"slu://a/b","wallet_file":"/predefined"},"services":{}}]} + + +*/