Skip to content

Commit

Permalink
update README, add comments
Browse files Browse the repository at this point in the history
  • Loading branch information
LukaGiorgadze committed May 1, 2023
1 parent 0066d04 commit d852dbf
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 48 deletions.
42 changes: 18 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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]
}
Expand All @@ -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)
}
// ...
}
Expand All @@ -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))
}


```
54 changes: 41 additions & 13 deletions gonull.go
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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
}

Expand All @@ -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)
}
86 changes: 75 additions & 11 deletions gonull_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,26 @@ 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)
}

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",
Expand All @@ -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)
}
}
Expand All @@ -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,
},
Expand All @@ -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)
})
}
}

0 comments on commit d852dbf

Please sign in to comment.