From d852dbf5ae3ffb7b4bf5f2028f23477d97c9e12a Mon Sep 17 00:00:00 2001 From: Luka Giorgadze Date: Mon, 1 May 2023 23:03:56 +0400 Subject: [PATCH] update README, add comments --- README.md | 42 +++++++++++------------- gonull.go | 54 +++++++++++++++++++++++-------- gonull_test.go | 86 +++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 134 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 206dbdc..b531d46 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,15 @@ ## Go package simplifies nullable fields handling with Go Generics. -This package provides a generic nullable type implementation for use with Go's `database/sql` package. -It simplifies handling nullable fields in SQL databases by wrapping any data type with the `Nullable` type. -The Nullable type works with both basic and custom data types and implements the `sql.Scanner` and `driver.Valuer` interfaces, making it easy to use with the `database/sql` package. +Package gonull provides a generic `Nullable` type for handling nullable values in a convenient way. +This is useful when working with databases and JSON, where nullable values are common. +Unlike other nullable libraries, gonull leverages Go's generics feature, enabling it to work seamlessly with any data type, making it more versatile and efficient. -## Use case +## Advantages +- Use of Go's generics allows for a single implementation that works with any data type. +- Seamless integration with `database/sql` and JSON marshalling/unmarshalling. +- Reduces boilerplate code and improves code readability. -In a web application, you may have a user profile with optional fields like name, age, or whatever. These fields can be left empty by the user, and your database stores them as `NULL` values. Using the `Nullable` type from this library, you can easily handle these optional fields when scanning data from the database or inserting new records. By wrapping the data types of these fields with the `Nullable` type, you can handle `NULL` values without additional logic, making your code cleaner and more maintainable. ## Usage @@ -18,7 +20,6 @@ go get https://github.com/lomsa-dev/gonull ```go type User struct { - ID int Name null.Nullable[string] Age null.Nullable[int] } @@ -33,11 +34,11 @@ func main() { for rows.Next() { var user User - err := rows.Scan(&user.ID, &user.Name, &user.Age) + err := rows.Scan( &user.Name, &user.Age) if err != nil { log.Fatal(err) } - fmt.Printf("ID: %d, Name: %v, Age: %v\n", user.ID, user.Name.Val, user.Age.Val) + fmt.Printf("ID: %d, Name: %v, Age: %v\n", user.Name.Val, user.Age.Val) } // ... } @@ -47,32 +48,25 @@ Another example ```go type Person struct { - Age gonull.Nullable[int] `json:"age,omitempty"` - PhoneNumber gonull.Nullable[string] `json:"phone_number,omitempty"` + Name string + Age int + Address gonull.Nullable[string] } func main() { - // Create a Person with some nullable fields set - person := Person{ - Age: gonull.NewNullable(30), - PhoneNumber: gonull.Nullable[string]{}, // Not set - } + jsonData := []byte(`{"Name":"Alice","Age":30,"Address":null}`) - // Marshal the Person struct to JSON - jsonData, err := json.Marshal(person) + var person Person + err := json.Unmarshal(jsonData, &person) if err != nil { panic(err) } - fmt.Printf("Marshalled JSON: %s\n", jsonData) + fmt.Printf("Unmarshalled Person: %+v\n", person) - // Unmarshal the JSON data back to a Person struct - var personFromJSON Person - err = json.Unmarshal(jsonData, &personFromJSON) + marshalledData, err := json.Marshal(person) if err != nil { panic(err) } - fmt.Printf("Unmarshalled struct: %+v\n", personFromJSON) + fmt.Printf("Marshalled JSON: %s\n", string(marshalledData)) } - - ``` diff --git a/gonull.go b/gonull.go index 1ca1f37..04e97cc 100644 --- a/gonull.go +++ b/gonull.go @@ -1,3 +1,5 @@ +// Package gonull provides a generic Nullable type for handling nullable values in a convenient way. +// This is useful when working with databases and JSON, where nullable values are common. package gonull import ( @@ -6,53 +8,69 @@ import ( "errors" ) -// Nullable wraps a generic nullable type that can be used with Go's database/sql package. +var ( + // ErrUnsupportedConversion is an error that occurs when attempting to convert a value to an unsupported type. + // This typically happens when Scan is called with a value that cannot be converted to the target type T. + ErrUnsupportedConversion = errors.New("unsupported type conversion") +) + +// Nullable is a generic struct that holds a nullable value of any type T. +// It keeps track of the value (Val) and a flag (IsSet) indicating whether the value has been set. +// This allows for better handling of nullable values, ensuring proper value management and serialization. type Nullable[T any] struct { - Val T - IsSet bool + Val T + IsValid bool } -// NewNullable returns a new Nullable with the given value set and Valid set to true. +// NewNullable creates a new Nullable with the given value and sets IsSet to true. +// This is useful when you want to create a Nullable with an initial value, explicitly marking it as set. func NewNullable[T any](value T) Nullable[T] { - return Nullable[T]{Val: value, IsSet: true} + return Nullable[T]{Val: value, IsValid: true} } -// Scan implements the sql.Scanner interface. +// Scan implements the sql.Scanner interface for Nullable, allowing it to be used as a nullable field in database operations. +// It is responsible for properly setting the IsSet flag and converting the scanned value to the target type T. +// This enables seamless integration with database/sql when working with nullable values. func (n *Nullable[T]) Scan(value interface{}) error { if value == nil { - n.IsSet = false + n.IsValid = false return nil } var err error n.Val, err = convertToType[T](value) if err == nil { - n.IsSet = true + n.IsValid = true } return err } -// Value implements the driver.Valuer interface. +// Value implements the driver.Valuer interface for Nullable, enabling it to be used as a nullable field in database operations. +// This method ensures that the correct value is returned for serialization, handling unset Nullable values by returning nil. func (n Nullable[T]) Value() (driver.Value, error) { - if !n.IsSet { + if !n.IsValid { return nil, nil } return n.Val, nil } +// convertToType is a helper function that attempts to convert the given value to type T. +// This function is used by Scan to properly handle value conversion, ensuring that Nullable values are always of the correct type. func convertToType[T any](value interface{}) (T, error) { switch v := value.(type) { case T: return v, nil default: var zero T - return zero, errors.New("unsupported type conversion") + return zero, ErrUnsupportedConversion } } +// UnmarshalJSON implements the json.Unmarshaler interface for Nullable, allowing it to be used as a nullable field in JSON operations. +// This method ensures proper unmarshalling of JSON data into the Nullable value, correctly setting the IsSet flag based on the JSON data. func (n *Nullable[T]) UnmarshalJSON(data []byte) error { if string(data) == "null" { - n.IsSet = false + n.IsValid = false return nil } @@ -62,6 +80,16 @@ func (n *Nullable[T]) UnmarshalJSON(data []byte) error { } n.Val = value - n.IsSet = true + n.IsValid = true return nil } + +// MarshalJSON implements the json.Marshaler interface for Nullable, enabling it to be used as a nullable field in JSON operations. +// This method ensures proper marshalling of Nullable values into JSON data, representing unset values as null in the serialized output. +func (n Nullable[T]) MarshalJSON() ([]byte, error) { + if !n.IsValid { + return []byte("null"), nil + } + + return json.Marshal(n.Val) +} diff --git a/gonull_test.go b/gonull_test.go index 391e225..4da3497 100644 --- a/gonull_test.go +++ b/gonull_test.go @@ -11,7 +11,7 @@ func TestNewNullable(t *testing.T) { value := "test" n := NewNullable(value) - assert.True(t, n.IsSet) + assert.True(t, n.IsValid) assert.Equal(t, value, n.Val) } @@ -19,18 +19,18 @@ func TestNullableScan(t *testing.T) { tests := []struct { name string value interface{} - isSet bool + IsValid bool wantErr bool }{ { - name: "nil value", - value: nil, - isSet: false, + name: "nil value", + value: nil, + IsValid: false, }, { - name: "string value", - value: "test", - isSet: true, + name: "string value", + value: "test", + IsValid: true, }, { name: "unsupported type", @@ -48,8 +48,8 @@ func TestNullableScan(t *testing.T) { assert.Error(t, err) } else { assert.NoError(t, err) - assert.Equal(t, tt.isSet, n.IsSet) - if tt.isSet { + assert.Equal(t, tt.IsValid, n.IsValid) + if tt.IsValid { assert.Equal(t, tt.value, n.Val) } } @@ -72,7 +72,7 @@ func TestNullableValue(t *testing.T) { }, { name: "unset value", - nullable: Nullable[string]{IsSet: false}, + nullable: Nullable[string]{IsValid: false}, wantValue: nil, wantErr: nil, }, @@ -87,3 +87,67 @@ func TestNullableValue(t *testing.T) { }) } } + +func TestNullableUnmarshalJSON(t *testing.T) { + type testCase struct { + name string + jsonData []byte + expectedVal int + expectedIsValid bool + } + + testCases := []testCase{ + { + name: "ValuePresent", + jsonData: []byte(`123`), + expectedVal: 123, + expectedIsValid: true, + }, + { + name: "ValueNull", + jsonData: []byte(`null`), + expectedVal: 0, + expectedIsValid: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var nullable Nullable[int] + + err := nullable.UnmarshalJSON(tc.jsonData) + assert.NoError(t, err) + assert.Equal(t, tc.expectedVal, nullable.Val) + assert.Equal(t, tc.expectedIsValid, nullable.IsValid) + }) + } +} + +func TestNullableMarshalJSON(t *testing.T) { + type testCase struct { + name string + nullable Nullable[int] + expectedJSON []byte + } + + testCases := []testCase{ + { + name: "ValuePresent", + nullable: NewNullable[int](123), + expectedJSON: []byte(`123`), + }, + { + name: "ValueNull", + nullable: Nullable[int]{Val: 0, IsValid: false}, + expectedJSON: []byte(`null`), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + jsonData, err := tc.nullable.MarshalJSON() + assert.NoError(t, err) + assert.Equal(t, tc.expectedJSON, jsonData) + }) + } +}