dtokit

package
v0.244.1 Latest Latest
Warning

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

Go to latest
Published: Sep 28, 2024 License: Apache-2.0 Imports: 7 Imported by: 3

README

Data Transfer Objects (DTOs) Package

This package provides an easy-to-use and maintainable way to create DTOs and map between entities and their corresponding DTOs in Go applications. The package simplifies the process of creating DTOs, while ensuring type safety and reducing the potential for errors.

Features

  • Simplified creation of Data Transfer Objects (DTOs)
  • Mapping between entities and DTOs using custom mapping functions
  • Automatic handling of nested entities
  • Type-safe DTO creation and mapping
  • Support for multiple DTO mappings for different serialization formats, such as JSON or YAML
  • Ability to register multiple DTOs for a single entity

Getting Started

To get started, import the package into your Go application:

import "go.llib.dev/frameless/pkg/dtokit"
Registering Mapping

The dtokit.Register function is used to register the mappings between entities and their respective dtokit. In this example, we're using a JSONMapping object to contain the mapping configurations:

var JSONMapping dtokit.M
var _ = dtokit.Register[Ent, EntDTO](&JSONMapping, EntMapping{})
var _ = dtokit.Register[NestedEnt, NestedEntDTO](&JSONMapping, NestedEntMapping{})
Defining Mappings

The mappings are defined using custom mapping functions for each entity-DTO pair:

package exampe

type Ent struct {
	V int
}

type EntDTO struct {
	V string `json:"v"`
}

type EntMapping struct{}

func (EntMapping) ToDTO(_ *dtokit.M, ent Ent) (EntDTO, error) {
	return EntDTO{V: strconv.Itoa(ent.V)}, nil
}

func (EntMapping) ToEnt(m *dtokit.M, dto EntDTO) (Ent, error) {
	v, err := strconv.Atoi(dto.V)
	if err != nil {
		return Ent{}, err
	}
	return Ent{V: v}, nil
}
Using Different Mapping Registries Per Serialization Formats

The package allows you to register different mappings for different serialization formats, such as JSON or YAML, or for different use cases. This makes it easy to maintain and update your mappings over time even if got a larger number of DTO models.

Here is an example of how to register a JSON-specific mapping and a YAML-specific mapping:

var JSONMapping dtokit.M
_ = dtokit.Register[Ent, EntJSONDTO](&JSONMapping, EntJSONMapping{})

var YAMLMapping dtokit.M
_ = dtokit.Register[Ent, EntYAMLDTO](&YAMLMapping, EntYAMLMapping{})

You can utilise distinct mapping registers as various mapping versions, simplifying the versioning process for your endpoint.

Using a Single Mapping Registry For Multiple Serialization Formats

The package also allows you to use a single mapping registry (dtokit.M) to manage all your DTO mappings. This can be useful when you have moderate number of DTOs, and want to keep things simple.

Here is an example:

var DTOMappings dtokit.M
var _ = dtokit.Register[Ent, EntJSONDTO](&DTOMappings, EntJSONMapping{})
var _ = dtokit.Register[Ent, EntYAMLDTO](&DTOMappings, EntYAMLMapping{})
Mapping Entities to DTOs & Marshalling

The dtokit.Map function is used to convert an entity object into a target DTO object.

Once the DTO has been created, it can be marshaled into a the target format. For Example in case of json you can use Go's built-in json.Marshal function:

package exampe

func fn() {
	var v = NestedEnt{
		ID: "42",
		Ent: Ent{
			V: 42,
		},
	}

	dto, err := dtokit.Map[NestedEntDTO](&JSONMapping, v)
	if err != nil { // handle err
		return
	}

	data, err := json.Marshal(dto)
	if err != nil { // handle error
		return
	}
	/*
		{
			"id": "42",
			"ent": {
				"v": "42"
			}
		}
	*/
}

Documentation

Index

Examples

Constants

View Source
const ErrNoMapping errorkit.Error = "[dtokit] missing mapping"

Variables

This section is empty.

Functions

func Map

func Map[To, From any](ctx context.Context, from From) (_ To, returnErr error)
Example
package main

import (
	"context"
	"strconv"

	"go.llib.dev/frameless/pkg/dtokit"
)

func main() {
	var _ = dtokit.Register[Ent, EntDTO]( // only once at the global level
		EntMapping{}.ToDTO,
		EntMapping{}.ToEnt,
	)
	var (
		ctx = context.Background()
		ent = Ent{V: 42, N: 12}
	)

	dto, err := dtokit.Map[EntDTO](ctx, ent)
	if err != nil {
		panic(err)
	}

	gotEnt, err := dtokit.Map[Ent](ctx, dto)
	if err != nil {
		panic(err)
	}

	_ = gotEnt == ent // true
}

type Ent struct {
	V int
	N int
}

type EntDTO struct {
	V string `json:"v"`
	N int    `json:"n"`
}

type EntMapping struct{}

func (EntMapping) ToDTO(ctx context.Context, ent Ent) (EntDTO, error) {
	return EntDTO{V: strconv.Itoa(ent.V), N: ent.N}, nil
}

func (EntMapping) ToEnt(ctx context.Context, dto EntDTO) (Ent, error) {
	v, err := strconv.Atoi(dto.V)
	if err != nil {
		return Ent{}, err
	}
	return Ent{V: v, N: dto.N}, nil
}
Example (SliceSyntaxSugar)
package main

import (
	"context"
	"strconv"

	"go.llib.dev/frameless/pkg/dtokit"
)

func main() {
	var _ = dtokit.Register[Ent, EntDTO]( // only once at the global level
		EntMapping{}.ToDTO,
		EntMapping{}.ToEnt,
	)
	var (
		ctx  = context.Background()
		ents = []Ent{{V: 42, N: 12}}
	)

	// all individual value will be mapped
	res, err := dtokit.Map[[]EntDTO](ctx, ents)
	if err != nil {
		panic(err)
	}
	_ = res // []EntDTO{V: "42", N: 12}
}

type Ent struct {
	V int
	N int
}

type EntDTO struct {
	V string `json:"v"`
	N int    `json:"n"`
}

type EntMapping struct{}

func (EntMapping) ToDTO(ctx context.Context, ent Ent) (EntDTO, error) {
	return EntDTO{V: strconv.Itoa(ent.V), N: ent.N}, nil
}

func (EntMapping) ToEnt(ctx context.Context, dto EntDTO) (Ent, error) {
	v, err := strconv.Atoi(dto.V)
	if err != nil {
		return Ent{}, err
	}
	return Ent{V: v, N: dto.N}, nil
}

func MustMap

func MustMap[To, From any](ctx context.Context, from From) To

func Register

func Register[From, To any](mapTo mapFunc[From, To], mapFrom mapFunc[To, From]) func()

Register function facilitates the registration of a mapping between two types. Optionally, if you don't intend to support bidirectional mapping, you can pass nil for the mapFrom argument. It's important to consider that supporting bidirectional mapping between an entity type and a DTO type often leads to the creation of non-partial DTO structures, enhancing their usability on the client side.

Example
package main

import (
	"context"
	"encoding/json"
	"strconv"

	"go.llib.dev/frameless/pkg/dtokit"
)

func main() {
	// JSONMapping will contain mapping from entities to JSON DTO structures.
	// registering Ent <---> EntDTO mapping
	_ = dtokit.Register[Ent, EntDTO](
		EntMapping{}.ToDTO,
		EntMapping{}.ToEnt,
	)
	// registering NestedEnt <---> NestedEntDTO mapping, which includes the mapping of the nested entities
	_ = dtokit.Register[NestedEnt, NestedEntDTO](
		NestedEntMapping{}.ToDTO,
		NestedEntMapping{}.ToEnt,
	)

	var v = NestedEnt{
		ID: "42",
		Ent: Ent{
			V: 42,
		},
	}

	ctx := context.Background()
	dto, err := dtokit.Map[NestedEntDTO](ctx, v)
	if err != nil { // handle err
		return
	}

	_ = dto // data mapped into a DTO and now ready for marshalling
	/*
		NestedEntDTO{
			ID: "42",
			Ent: EntDTO{
				V: "42",
			},
		}
	*/

	data, err := json.Marshal(dto)
	if err != nil { // handle error
		return
	}

	_ = data
	/*
		{
			"id": "42",
			"ent": {
				"v": "42"
			}
		}
	*/

}

type Ent struct {
	V int
	N int
}

type EntDTO struct {
	V string `json:"v"`
	N int    `json:"n"`
}

type EntMapping struct{}

func (EntMapping) ToDTO(ctx context.Context, ent Ent) (EntDTO, error) {
	return EntDTO{V: strconv.Itoa(ent.V), N: ent.N}, nil
}

func (EntMapping) ToEnt(ctx context.Context, dto EntDTO) (Ent, error) {
	v, err := strconv.Atoi(dto.V)
	if err != nil {
		return Ent{}, err
	}
	return Ent{V: v, N: dto.N}, nil
}

type NestedEnt struct {
	ID  string
	Ent Ent
}

type NestedEntDTO struct {
	ID  string `json:"id"`
	Ent EntDTO `json:"ent"`
}

type NestedEntMapping struct{}

func (NestedEntMapping) ToEnt(ctx context.Context, dto NestedEntDTO) (NestedEnt, error) {
	return NestedEnt{
		ID:  dto.ID,
		Ent: dtokit.MustMap[Ent](ctx, dto.Ent),
	}, nil
}

func (NestedEntMapping) ToDTO(ctx context.Context, ent NestedEnt) (NestedEntDTO, error) {
	return NestedEntDTO{
		ID:  ent.ID,
		Ent: dtokit.MustMap[EntDTO](ctx, ent.Ent),
	}, nil
}
Example (PartialDTOMappingSupport)
package main

import (
	"context"

	"go.llib.dev/frameless/pkg/dtokit"
)

func main() {
	// When we only need an Entity to EntityPartialDTO mapping.
	dtokit.Register[Ent, EntPartialDTO](EntToEntPartialDTO, nil)()

	var (
		ctx = context.Background()
		v   = Ent{V: 42, N: 12}
	)

	partialDTO, err := dtokit.Map[EntPartialDTO](ctx, v)
	_, _ = partialDTO, err
}

type Ent struct {
	V int
	N int
}

type EntPartialDTO struct {
	N int `json:"n"`
}

func EntToEntPartialDTO(ctx context.Context, ent Ent) (EntPartialDTO, error) {
	return EntPartialDTO{N: ent.N}, nil
}

Types

type ErrMustMap

type ErrMustMap struct{ Err error }

func (ErrMustMap) Error

func (err ErrMustMap) Error() string

type MK

type MK struct {
	From reflect.Type
	To   reflect.Type
}

type MP

type MP interface {
	FromType() reflect.Type
	ToType() reflect.Type
}

type Mapper added in v0.230.0

type Mapper[ENT any] interface {
	// NewiDTO should return back a new(DTO) value.
	// It is ideal to use with Unmarshal functions such as json.Unmarshal.
	//
	// The main reason for hiding the information of DTO type,
	// is because where you use a Mapper[Entity] interface
	// is often not aware what will be the DTO type,
	// especially with reusable generic code,
	// and most serializer implementation fine with accepting an "any" argument type.
	NewiDTO() (dtoPtr any)
	// MapFromiDTOPtr expected to map a *DTO value to an Entity value.
	// We use a DTO pointer here, because the user of Mapper doesn't know the type as well,
	// it just use the result of NewiDTO as an argument to Unmarshal.
	//
	// 		dtoPtr := m.NewiDTO()
	// 		_ = json.Unmarshal(data, dtoPtr)
	// 		ent, err := m.MapFromiDTOPtr(ctx, dtoPtr)
	//
	MapFromiDTOPtr(ctx context.Context, dtoPtr any) (ENT, error)
	// MapToiDTO expected to map an ENT value to a DTO value.
	// The result of this often passed to a Marshal function.
	//
	// 		dto, _ := m.MapToiDTO(ctx, ent)
	// 		data, _ := json.Marshal(dto)
	//
	MapToiDTO(context.Context, ENT) (DTO any, _ error)
}

Mapper is a generic interface used for representing a Entity-DTO mapping relationship where the user of the Mapper is not aware of the concrete type of the DTO. This could be a case for when the user of the mapper simply uses the dto to pass to a marshaling/unmarshaling function.

Implemented by Mapping[Entity, DTO]

type MapperTo added in v0.240.0

type MapperTo[ENT, DTO any] interface {
	MapToDTO(context.Context, ENT) (DTO, error)
	MapToENT(context.Context, DTO) (ENT, error)
}

MapperTo[ENT, DTO] represents a mapping between ENT and DTO. It is a generic way to express a mapping relationship when the user of the mapping is aware of the concrete DTO type.

Implemented by Mapping[Entity, DTO]

type Mapping added in v0.230.0

type Mapping[ENT, DTO any] struct {
	// ToENT is an optional function to describe how to map a DTO into an Entity.
	//
	// default: dtokit.Map[Entity, DTO](...)
	ToENT func(ctx context.Context, dto DTO) (ENT, error)
	// ToDTO is an optional function to describe how to map an Entity into a DTO.
	//
	// default: dtokit.Map[DTO, Entity](...)
	ToDTO func(ctx context.Context, ent ENT) (DTO, error)
}

Mapping is a type safe implementation to represent a Mapping between an ENT and its DTO type. Mapping[ENT, DTO] implements Mapper[ENT].

func (Mapping[ENT, DTO]) MapFromiDTOPtr added in v0.240.0

func (m Mapping[ENT, DTO]) MapFromiDTOPtr(ctx context.Context, dtoPtr any) (ENT, error)

func (Mapping[ENT, DTO]) MapToDTO added in v0.230.0

func (m Mapping[ENT, DTO]) MapToDTO(ctx context.Context, ent ENT) (DTO, error)

func (Mapping[ENT, DTO]) MapToENT added in v0.240.0

func (m Mapping[ENT, DTO]) MapToENT(ctx context.Context, dto DTO) (ENT, error)

func (Mapping[ENT, DTO]) MapToiDTO added in v0.240.0

func (m Mapping[ENT, DTO]) MapToiDTO(ctx context.Context, ent ENT) (any, error)

func (Mapping[ENT, DTO]) NewiDTO added in v0.240.0

func (m Mapping[ENT, DTO]) NewiDTO() any

type P

type P[A, B any] struct{}

P is a Mapping pair

func (P[A, B]) FromType

func (p P[A, B]) FromType() reflect.Type

FromType specify that P[A] is the form type.

func (P[A, B]) MapA

func (p P[A, B]) MapA(ctx context.Context, v B) (A, error)

func (P[A, B]) MapB

func (p P[A, B]) MapB(ctx context.Context, v A) (B, error)

func (P[A, B]) NewA

func (p P[A, B]) NewA() *A

func (P[A, B]) NewB

func (p P[A, B]) NewB() *B

func (P[A, B]) ToType

func (p P[A, B]) ToType() reflect.Type

ToType specify that P[B] is the form type.

Jump to

Keyboard shortcuts

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