smapper

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 4, 2024 License: MIT Imports: 5 Imported by: 0

README

smapper

Go Report Card tests workflow GitHub Actions Workflow Status Go Reference

A flexible and easy-to-use struct-to-struct mapping library allows you to convert an arbitrary type into another arbitrary type. It supports custom validations to ensure data consistency, automatically converts compatible types, and gives you control over incompatible types (e.g. string to time.Time) using callbacks you specify per each field.

Installation

Using go get

$ go get github.com/alir32a/smapper

Usages and Examples

Convert two similar structs

using Map

package main

import (
	"fmt"
	"github.com/alir32a/smapper"
)

type User struct {
	ID       uint
	Username string
}

type Person struct {
	ID   int64 `smapper:"-"` // will be skipped
	UserID uint `smapper:"ID"`
	Name string `smapper:"username"`
}

func main() {
	mapper := smapper.New()

	user := User{
		ID:       42,
		Username: "alir32a",
	}
	person := Person{}

	err := mapper.Map(user, &person)
	if err != nil {
		panic(err)
	}

	fmt.Println(person.ID) // 0
	fmt.Println(person.UserID)   // 42
	fmt.Println(person.Name) // alir32a
}

or using MapTo

package main

import (
	"fmt"
	"github.com/alir32a/smapper"
)

type User struct {
	ID       uint
	Username string
}

type Person struct {
	ID   int64 `smapper:"-"` // will be skipped
	UserID uint `smapper:"ID"`
	Name string `smapper:"username"`
}

func main() {
	user := User{
		ID:       42,
		Username: "alir32a",
	}

	person, err := smapper.MapTo[Person](user)
	if err != nil {
		panic(err)
	}

	fmt.Println(person.ID) // 0
	fmt.Println(person.UserID)   // 42
	fmt.Println(person.Name) // alir32a
}
Validations
package main

import (
	"fmt"
	"reflect"
	"regexp"
	"github.com/alir32a/smapper"
	"strings"
)

type User struct {
	ID       uint
	Username string
	Email    string
}

type Person struct {
	ID     uint   `smapper:"-"` // will be skipped
	UserID int64  `smapper:",required"`
	Name   string `smapper:"username,eq=admin"`
	Email  string `smapper:",contains=gmail"`
}

func main() {
	mapper := smapper.New(smapper.WithValidators(Contains()))
    
	user := User{
		ID: 42, // cannot be 0
		Name: "admin", // should be equal to admin
		Email: "admin@gmail.com", // must contain "gmail"
    }
	person := Person{}
	
	err := mapper.Map(user, &person)
	if err != nil {
        panic("should be nil")
	}
}

func Contains() *smapper.Validator {
	return smapper.NewValidator("contains", func(v reflect.Value, param string) bool {
		if v.Kind() != reflect.String {
			panic("expected string")
		}

		return strings.Contains(v.String(), param)
	})
}

You can define your own validators, just like we did here. We created a function to check if the value contains the required parameter ('gmail'). If it doesn't, validation fails, resulting in a validation error. You can use multiple validators on a single field, combining built-in validators with your custom ones.

Built-in Validators
Tag Name Description Accept Parameter
required Field must not be zero value No
unique Field must have unique values
(can be used with slices, arrays and maps)
No
len Field must have the given length Yes
eq Field must be equal to the given param Yes
ne Field must not be equal to the given param Yes
gte Field's value or length must be greater than or equal to the given param Yes
gt Field's value or length must be greater than the given param Yes
lte Field's value or length must be less than or equal to the given param Yes
lt Field's value or length must be greater than the given param Yes

To override built-in validators, set Mapper.OverrideDefaultValidators = true or use WithOverrideDefaultValidators() during initialization.

Callbacks
package main

import (
	"errors"
	"fmt"
	"reflect"
	"regexp"
	"github.com/alir32a/smapper"
	"strconv"
)

type User struct {
	ID       int64
	Username string
	Email    string
}

type Person struct {
	ID     uint    `smapper:"-"`
	UserID string  `smapper:"ID,callback:to_string"`
	Name   string  `smapper:"username"`
	Email  string
}

func main() {
	mapper := smapper.New(smapper.WithCallbacks(ToString()))

	user := User{
		ID:       42,                // converts to "42"
		Username: "admin",
		Email:    "admin@gmail.com",
	}
	person := Person{}

	err := mapper.Map(user, &person)
	if err != nil {
		panic("should be nil")
	}
	
	fmt.Println(person.UserID) // 42
}

func ToString() *smapper.Callback {
	return smapper.NewCallback("to_string", func(src reflect.Type, target reflect.Type, v any) (any, error) {
		if target.Kind() != reflect.String {
			return v, errors.New("target type is not string")
		}

		switch src.Kind() {
		case reflect.Int64:
			return strconv.FormatInt(v.(int64), 10), nil
		case reflect.Uint64:
			return strconv.FormatUint(v.(uint64), 10), nil
		case reflect.Float64:
			return strconv.FormatFloat(v.(float64), 'f', -1, 64), nil
		case reflect.String:
			return v, nil
		default:
            return v, errors.New("unsupported type")
		}
	})
}

you can execute only one callback per field.

we defined a callback named to_string, to map an int64 field to string. because int64 and strings are incompatible (you cannot use go type conversion like string(int64)), you need to define a callback to do this conversion. as you can see, you have access to both input and output types in the callback function, so you can safely do the conversion.

smapper can automatically convert between strings and numbers, but it's disabled by default, to enable it, you need to set AutoStringToNumberConversion = true and/or AutoNumberToStringConversion = true, or use WithAutoStringToNumberConversion() and/or WithAutoNumberToStringConversion() when initializing a new mapper.

Nested Structures
package main

import (
	"errors"
	"fmt"
	"reflect"
	"regexp"
	"github.com/alir32a/smapper"
	"strconv"
	"time"
)
type PurchasedItem struct {
	ProductID uint
	Name      string
}

type ReturnedItem struct {
	ProductID uint
	Name      string
}

type Address struct {
	Title string
	Detail string
}

type Purchase struct {
	Items       []PurchasedItem
	Source      Address
	Destination Address
}

type Return struct {
	Items       []ReturnedItem
	Source      Address `smapper:"destination"`
	Destination Address `smapper:"source"`
}

func main() {
	mapper := smapper.New()

	home := Address{Title: "Home", Detail: "Pine St"}
	bakery := Address{Title: "Bakery", Detail: "East Ave"}
	pie := PurchasedItem{ProductID: 42, Name: "Apple pie"}

	purchase := Purchase{
		Items:       []PurchasedItem{pie},
		Source:      bakery,
		Destination: home,
	}
	ret := Return{}

	err := mapper.Map(purchase, &ret)
	if err != nil {
		panic("should be nil")
	}

	fmt.Printf("%+v\n", ret.Items)     // [{ProductID:42 Name:Apple pie}]
	fmt.Println(ret.Source.Title)      // Home
	fmt.Println(ret.Destination.Title) // Bakery
}

Contribution

Thanks for taking the time to contribute. Please see CONTRIBUTING.md.

Donations

Thank you for your encouraging support, It helps me build more things.

bitcoin: bc1q44ffnc274fg0j982tjqthrn9ka85l8hhntgj82

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Map

func Map(input, output any, opts ...Option) error

Map initialize a new Mapper with the given options and executes the Mapper.Map.

func MapTo added in v0.2.0

func MapTo[T any](input any, opts ...Option) (*T, error)

MapTo initializes a new Mapper with the provided options, maps the input value to a new instance of type T, and returns a pointer to the mapped value.

func MapToWith added in v0.2.0

func MapToWith[T any](mapper *Mapper, input any) (*T, error)

MapToWith maps the input value to a new instance of type T using the provided mapper, and returns a pointer to the mapped value.

Types

type Callback

type Callback struct {
	Name string
	Func CallbackFunc
}

func NewCallback

func NewCallback(name string, fn CallbackFunc) *Callback

type CallbackError

type CallbackError struct {
	// contains filtered or unexported fields
}

func (*CallbackError) Error

func (e *CallbackError) Error() string

type CallbackFunc

type CallbackFunc func(reflect.Type, reflect.Type, any) (any, error)

type Config

type Config struct {
	// it allows you to override the default validators (e.g. eq, required, etc.) and use your
	// custom validator. if it's false (by default), then it ignores your validator and executes
	// the default validator.
	OverrideDefaultValidators bool
	// if you specify a validator in a field's tag that does not exist, you will get an error (by default),
	// but this allows you to ignore those missing validators.
	IgnoreMissingValidators bool
	// if you specify a callback in a field's tag that does not exist, you will get an error (by default),
	// but this allows you to ignore those missing callbacks.
	IgnoreMissingCallbacks bool
	// if you try to map a string value to a numeric value (int, uint or float), you will get an error (by default),
	// but this allows you to automatically convert strings to numbers.
	AutoStringToNumberConversion bool
	// if you try to map a numeric value (int, uint or float) to a string, you will get an error (by default),
	// but this allows you to automatically convert numbers to strings.
	AutoNumberToStringConversion bool
}

type Error

type Error struct {
	// contains filtered or unexported fields
}

func (*Error) Error

func (e *Error) Error() string

type FieldError

type FieldError struct {
	// contains filtered or unexported fields
}

func (*FieldError) Error

func (e *FieldError) Error() string

type FieldValue

type FieldValue struct {
	reflect.Value
	ParentType reflect.Type
	FieldName  string
}

func NewFieldValue

func NewFieldValue(value reflect.Value, parent reflect.Type, name string) FieldValue

func (FieldValue) From

func (f FieldValue) From(v reflect.Value) FieldValue

type Mapper

type Mapper struct {
	Config
	// contains filtered or unexported fields
}

func New

func New(opts ...Option) *Mapper

New returns a new Mapper with the given options.

func (*Mapper) Map

func (m *Mapper) Map(input, output any) error

Map takes a struct and converts it into another struct, output type must be a pointer to a struct.

Example
package main

import (
	"fmt"
	"github.com/alir32a/smapper"
)

func main() {
	type Person struct {
		ID       uint
		Username string
	}

	type User struct {
		ID   int64
		Name string `smapper:"username"`
	}

	mapper := smapper.New()

	person := Person{
		ID:       42,
		Username: "alir32a",
	}
	user := User{}

	err := mapper.Map(person, &user)
	if err != nil {
		panic(err)
	}

	fmt.Println(user.ID)   // 42
	fmt.Println(user.Name) // alir32a
}
Output:

Example (Callbacks)
package main

import (
	"errors"
	"fmt"
	"github.com/alir32a/smapper"
	"reflect"
	"strings"
)

func main() {
	type Person struct {
		ID       uint
		Username string
	}

	type User struct {
		ID   int64
		Name string `smapper:"Username,callback:uppercase"`
	}

	person := Person{
		ID:       42,
		Username: "admin",
	}
	user := User{}

	mapper := smapper.New(smapper.WithCallbacks(
		smapper.NewCallback("uppercase", func(src reflect.Type, dst reflect.Type, v any) (any, error) {
			// you have access to source and destination types here
			if src.Kind() != reflect.String || dst.Kind() != reflect.String {
				return nil, errors.New("wrong type")
			}

			return strings.ToUpper(v.(string)), nil
		},
		)))

	err := mapper.Map(person, &user)
	if err != nil {
		panic(err)
	}

	fmt.Println(user.Name) // ADMIN
}
Output:

Example (Validators)
package main

import (
	"fmt"
	"github.com/alir32a/smapper"
)

func main() {
	type Person struct {
		ID          uint
		Username    string
		PhoneNumber string
	}

	type User struct {
		ID          int64  `smapper:",required"`
		Name        string `smapper:"username,eq=admin"`
		PhoneNumber string `smapper:",len=8"`
	}

	p := Person{
		ID:          42,         // should not be 0
		Username:    "admin",    // must be equal to "admin"
		PhoneNumber: "12345678", // must have exactly 8 characters
	}
	user := User{}

	err := smapper.Map(p, &user) // must be ok
	if err != nil {
		panic(err)
	}

	p.ID = 0
	p.Username = "common"
	p.PhoneNumber = "1234567"

	err = smapper.Map(p, &user) // returns validation error
	if err != nil {
		fmt.Println(err.Error())
	}
}
Output:

type Option

type Option func(*Mapper)

func WithAutoNumberToStringConversion

func WithAutoNumberToStringConversion() Option

WithAutoNumberToStringConversion if you set this option, you can automatically convert numbers (int, uint and floats) to strings.

func WithAutoStringToNumberConversion

func WithAutoStringToNumberConversion() Option

WithAutoStringToNumberConversion if you set this option, you can automatically convert strings to numbers (int, uint and floats)

func WithCallbacks

func WithCallbacks(callbacks ...*Callback) Option

func WithIgnoreMissingCallbacks

func WithIgnoreMissingCallbacks() Option

WithIgnoreMissingCallbacks if you set this option, you can ignore errors that occur when you're trying to use a missing callback in a field's tag.

func WithIgnoreMissingValidators

func WithIgnoreMissingValidators() Option

WithIgnoreMissingValidators if you set this option, you can ignore errors that occur when you're trying to use a missing validator in a field's tag.

func WithOverrideDefaultValidators

func WithOverrideDefaultValidators() Option

WithOverrideDefaultValidators if you set this option, you can override default validators (e.g. required, len, etc.) and your validator func will being use instead.

func WithValidators

func WithValidators(validators ...*Validator) Option

type ValidationError

type ValidationError struct {
	// contains filtered or unexported fields
}

func (*ValidationError) Error

func (e *ValidationError) Error() string

type Validator

type Validator struct {
	Name string
	Func ValidatorFunc
}

func NewValidator

func NewValidator(name string, fn ValidatorFunc) *Validator

type ValidatorFunc

type ValidatorFunc func(reflect.Value, string) bool

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL