Remove ulele/deepcopier in favor of JSON deep copy

We have a very high performance JSON library that doesn't need to
perform code generation. Let's use it instead of our questionably
performant, reflection-dependent deep copy library.

Most changes because some functions can now return errors.

Also converts cmd/podman to use jsoniter, instead of pkg/json,
for increased performance.

Signed-off-by: Matthew Heon <matthew.heon@pm.me>
This commit is contained in:
Matthew Heon
2019-03-25 15:43:38 -04:00
parent 340eeec1b6
commit 5ed62991dc
27 changed files with 106 additions and 558 deletions

View File

@ -96,9 +96,14 @@ func commitCmd(c *cliconfig.CommitValues) error {
return errors.Wrapf(err, "error looking up container %q", container)
}
sc := image.GetSystemContext(runtime.GetConfig().SignaturePolicyPath, "", false)
rtc, err := runtime.GetConfig()
if err != nil {
return err
}
sc := image.GetSystemContext(rtc.SignaturePolicyPath, "", false)
coptions := buildah.CommitOptions{
SignaturePolicyPath: runtime.GetConfig().SignaturePolicyPath,
SignaturePolicyPath: rtc.SignaturePolicyPath,
ReportWriter: writer,
SystemContext: sc,
PreferredManifestType: mimeType,

View File

@ -12,12 +12,14 @@ import (
"github.com/containers/libpod/pkg/rootless"
"github.com/containers/storage"
"github.com/fatih/camelcase"
jsoniter "github.com/json-iterator/go"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
var (
stores = make(map[storage.Store]struct{})
json = jsoniter.ConfigCompatibleWithStandardLibrary
)
const (

View File

@ -2,7 +2,6 @@ package main
import (
"context"
"encoding/json"
"strings"
"github.com/containers/buildah/pkg/formats"

View File

@ -71,7 +71,11 @@ func mountCmd(c *cliconfig.MountValues) error {
defer runtime.Shutdown(false)
if os.Geteuid() != 0 {
if driver := runtime.GetConfig().StorageConfig.GraphDriverName; driver != "vfs" {
rtc, err := runtime.GetConfig()
if err != nil {
return err
}
if driver := rtc.StorageConfig.GraphDriverName; driver != "vfs" {
// Do not allow to mount a graphdriver that is not vfs if we are creating the userns as part
// of the mount command.
return fmt.Errorf("cannot mount using driver %s in rootless mode", driver)

View File

@ -1,7 +1,6 @@
package main
import (
"encoding/json"
"fmt"
"github.com/containers/libpod/cmd/podman/cliconfig"

View File

@ -9,7 +9,6 @@ import (
"text/tabwriter"
"time"
"encoding/json"
tm "github.com/buger/goterm"
"github.com/containers/buildah/pkg/formats"
"github.com/containers/libpod/cmd/podman/cliconfig"
@ -17,7 +16,6 @@ import (
"github.com/containers/libpod/pkg/adapter"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/ulule/deepcopier"
)
var (
@ -187,7 +185,9 @@ func podStatsCmd(c *cliconfig.PodStatsValues) error {
}
time.Sleep(time.Second)
previousPodStats := new([]*libpod.PodContainerStats)
deepcopier.Copy(newStats).To(previousPodStats)
if err := libpod.JSONDeepCopy(newStats, previousPodStats); err != nil {
return err
}
pods, err = runtime.GetStatPods(c)
if err != nil {
return err

View File

@ -1,7 +1,6 @@
package main
import (
"encoding/json"
"fmt"
"html/template"
"os"

View File

@ -154,7 +154,11 @@ func runCmd(c *cliconfig.RunValues) error {
if errors.Cause(err) == libpod.ErrNoSuchCtr {
// The container may have been removed
// Go looking for an exit file
ctrExitCode, err := readExitFile(runtime.GetConfig().TmpDir, ctr.ID())
rtc, err := runtime.GetConfig()
if err != nil {
return err
}
ctrExitCode, err := readExitFile(rtc.TmpDir, ctr.ID())
if err != nil {
logrus.Errorf("Cannot get exit code: %v", err)
exitCode = 127

View File

@ -43,20 +43,23 @@ func getContext() context.Context {
func CreateContainer(ctx context.Context, c *cliconfig.PodmanCommand, runtime *libpod.Runtime) (*libpod.Container, *cc.CreateConfig, error) {
var (
healthCheck *manifest.Schema2HealthConfig
err error
cidFile *os.File
)
if c.Bool("trace") {
span, _ := opentracing.StartSpanFromContext(ctx, "createContainer")
defer span.Finish()
}
rtc := runtime.GetConfig()
rtc, err := runtime.GetConfig()
if err != nil {
return nil, nil, err
}
rootfs := ""
if c.Bool("rootfs") {
rootfs = c.InputArgs[0]
}
var err error
var cidFile *os.File
if c.IsSet("cidfile") && os.Geteuid() == 0 {
cidFile, err = libpod.OpenExclusiveFile(c.String("cidfile"))
if err != nil && os.IsExist(err) {
@ -721,7 +724,11 @@ func ParseCreateOpts(ctx context.Context, c *cliconfig.PodmanCommand, runtime *l
if c.Bool("init") {
initPath := c.String("init-path")
if initPath == "" {
initPath = runtime.GetConfig().InitPath
rtc, err := runtime.GetConfig()
if err != nil {
return nil, err
}
initPath = rtc.InitPath
}
if err := config.AddContainerInitBinary(initPath); err != nil {
return nil, err

View File

@ -108,7 +108,11 @@ func signCmd(c *cliconfig.SignValues) error {
}
// create the signstore file
newImage, err := runtime.ImageRuntime().New(getContext(), signimage, runtime.GetConfig().SignaturePolicyPath, "", os.Stderr, nil, image.SigningOptions{SignBy: signby}, false, nil)
rtc, err := runtime.GetConfig()
if err != nil {
return err
}
newImage, err := runtime.ImageRuntime().New(getContext(), signimage, rtc.SignaturePolicyPath, "", os.Stderr, nil, image.SigningOptions{SignBy: signby}, false, nil)
if err != nil {
return errors.Wrapf(err, "error pulling image %s", signimage)
}

View File

@ -129,7 +129,11 @@ func startCmd(c *cliconfig.StartValues) error {
if errors.Cause(err) == libpod.ErrNoSuchCtr {
// The container may have been removed
// Go looking for an exit file
ctrExitCode, err := readExitFile(runtime.GetConfig().TmpDir, ctr.ID())
rtc, err := runtime.GetConfig()
if err != nil {
return err
}
ctrExitCode, err := readExitFile(rtc.TmpDir, ctr.ID())
if err != nil {
logrus.Errorf("Cannot get exit code: %v", err)
exitCode = 127

View File

@ -1,7 +1,6 @@
package main
import (
"encoding/json"
"io/ioutil"
"os"
"sort"

View File

@ -151,7 +151,6 @@ Provides: bundled(golang(github.com/stretchr/testify)) = 4d4bfba8f1d1027c4fdbe37
Provides: bundled(golang(github.com/syndtr/gocapability)) = e7cb7fa329f456b3855136a2642b197bad7366ba
Provides: bundled(golang(github.com/tchap/go-patricia)) = v2.2.6
Provides: bundled(golang(github.com/ulikunitz/xz)) = v0.5.4
Provides: bundled(golang(github.com/ulule/deepcopier)) = master
# "-" are not accepted in version strings, so comment out below line
#Provides: bundled(golang(github.com/urfave/cli)) = fix-short-opts-parsing
Provides: bundled(golang(github.com/varlink/go)) = master
@ -237,7 +236,6 @@ BuildRequires: golang(github.com/opencontainers/selinux/go-selinux)
BuildRequires: golang(github.com/opencontainers/selinux/go-selinux/label)
BuildRequires: golang(github.com/pkg/errors)
BuildRequires: golang(github.com/sirupsen/logrus)
BuildRequires: golang(github.com/ulule/deepcopier)
BuildRequires: golang(golang.org/x/crypto/ssh/terminal)
BuildRequires: golang(golang.org/x/sys/unix)
BuildRequires: golang(k8s.io/apimachinery/pkg/util/wait)
@ -290,7 +288,6 @@ Requires: golang(github.com/opencontainers/selinux/go-selinux)
Requires: golang(github.com/opencontainers/selinux/go-selinux/label)
Requires: golang(github.com/pkg/errors)
Requires: golang(github.com/sirupsen/logrus)
Requires: golang(github.com/ulule/deepcopier)
Requires: golang(golang.org/x/crypto/ssh/terminal)
Requires: golang(golang.org/x/sys/unix)
Requires: golang(k8s.io/apimachinery/pkg/util/wait)

View File

@ -17,7 +17,6 @@ import (
"github.com/cri-o/ocicni/pkg/ocicni"
spec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/ulule/deepcopier"
)
// ContainerStatus represents the current state of a container
@ -407,7 +406,9 @@ func (t ContainerStatus) String() string {
// Config returns the configuration used to create the container
func (c *Container) Config() *ContainerConfig {
returnConfig := new(ContainerConfig)
deepcopier.Copy(c.config).To(returnConfig)
if err := JSONDeepCopy(c.config, returnConfig); err != nil {
return nil
}
return returnConfig
}
@ -417,7 +418,9 @@ func (c *Container) Config() *ContainerConfig {
// spec may differ slightly as mounts are added based on the image
func (c *Container) Spec() *spec.Spec {
returnSpec := new(spec.Spec)
deepcopier.Copy(c.config.Spec).To(returnSpec)
if err := JSONDeepCopy(c.config.Spec, returnSpec); err != nil {
return nil
}
return returnSpec
}
@ -1094,7 +1097,9 @@ func (c *Container) ContainerState() (*ContainerState, error) {
}
}
returnConfig := new(ContainerState)
deepcopier.Copy(c.state).To(returnConfig)
if err := JSONDeepCopy(c.state, returnConfig); err != nil {
return nil, errors.Wrapf(err, "error copying container %s state", c.ID())
}
return c.state, nil
}

View File

@ -58,6 +58,10 @@ func (v *Volume) newVolumeEvent(status events.Status) {
// Events is a wrapper function for everyone to begin tailing the events log
// with options
func (r *Runtime) Events(fromStart, stream bool, options []events.EventFilter, eventChannel chan *events.Event) error {
if !r.valid {
return ErrRuntimeStopped
}
t, err := r.getTail(fromStart, stream)
if err != nil {
return err
@ -71,7 +75,7 @@ func (r *Runtime) Events(fromStart, stream bool, options []events.EventFilter, e
case events.Image, events.Volume, events.Pod, events.Container:
// no-op
default:
return errors.Errorf("event type %s is not valid in %s", event.Type.String(), r.GetConfig().EventsLogFilePath)
return errors.Errorf("event type %s is not valid in %s", event.Type.String(), r.config.EventsLogFilePath)
}
include := true
for _, filter := range options {

View File

@ -6,7 +6,6 @@ import (
"github.com/containers/libpod/libpod/events"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/ulule/deepcopier"
)
// Start starts all containers within a pod
@ -441,7 +440,9 @@ func (p *Pod) Inspect() (*PodInspect, error) {
infraContainerID := p.state.InfraContainerID
config := new(PodConfig)
deepcopier.Copy(p.config).To(config)
if err := JSONDeepCopy(p.config, config); err != nil {
return nil, err
}
inspectData := PodInspect{
Config: config,
State: &PodInspectState{

View File

@ -23,7 +23,6 @@ import (
"github.com/docker/docker/pkg/namesgenerator"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/ulule/deepcopier"
)
// RuntimeStateStore is a constant indicating which state store implementation
@ -355,7 +354,9 @@ func newRuntimeFromConfig(userConfigPath string, options ...RuntimeOption) (runt
if err != nil {
return nil, err
}
deepcopier.Copy(defaultRuntimeConfig).To(runtime.config)
if err := JSONDeepCopy(defaultRuntimeConfig, runtime.config); err != nil {
return nil, errors.Wrapf(err, "error copying runtime default config")
}
runtime.config.TmpDir = tmpDir
storageConf, err := util.GetDefaultStoreOptions()
@ -923,20 +924,22 @@ func makeRuntime(runtime *Runtime) (err error) {
}
// GetConfig returns a copy of the configuration used by the runtime
func (r *Runtime) GetConfig() *RuntimeConfig {
func (r *Runtime) GetConfig() (*RuntimeConfig, error) {
r.lock.RLock()
defer r.lock.RUnlock()
if !r.valid {
return nil
return nil, ErrRuntimeStopped
}
config := new(RuntimeConfig)
// Copy so the caller won't be able to modify the actual config
deepcopier.Copy(r.config).To(config)
if err := JSONDeepCopy(r.config, config); err != nil {
return nil, errors.Wrapf(err, "error copying config")
}
return config
return config, nil
}
// Shutdown shuts down the runtime and associated containers and storage

View File

@ -19,7 +19,6 @@ import (
opentracing "github.com/opentracing/opentracing-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/ulule/deepcopier"
)
// CtrRemoveTimeout is the default number of seconds to wait after stopping a container
@ -63,7 +62,9 @@ func (r *Runtime) newContainer(ctx context.Context, rSpec *spec.Spec, options ..
ctr.config.ID = stringid.GenerateNonCryptoID()
ctr.config.Spec = new(spec.Spec)
deepcopier.Copy(rSpec).To(ctr.config.Spec)
if err := JSONDeepCopy(rSpec, ctr.config.Spec); err != nil {
return nil, errors.Wrapf(err, "error copying runtime spec while creating container")
}
ctr.config.CreatedTime = time.Now()
ctr.config.ShmSize = DefaultShmSize

View File

@ -187,3 +187,13 @@ func validPodNSOption(p *Pod, ctrPod string) error {
}
return nil
}
// JSONDeepCopy performs a deep copy by performing a JSON encode/decode of the
// given structures. From and To should be identically typed structs.
func JSONDeepCopy(from, to interface{}) error {
tmp, err := json.Marshal(from)
if err != nil {
return err
}
return json.Unmarshal(tmp, to)
}

View File

@ -14,7 +14,6 @@ import (
"github.com/containers/libpod/libpod"
"github.com/containers/libpod/pkg/varlinkapi"
"github.com/pkg/errors"
"github.com/ulule/deepcopier"
)
// Pod ...
@ -99,7 +98,9 @@ func (r *LocalRuntime) LookupPod(nameOrID string) (*Pod, error) {
// the data of a remotepod data struct
func (p *Pod) Inspect() (*libpod.PodInspect, error) {
config := new(libpod.PodConfig)
deepcopier.Copy(p.remotepod.config).To(config)
if err := libpod.JSONDeepCopy(p.remotepod.config, config); err != nil {
return nil, err
}
inspectData := libpod.PodInspect{
Config: config,
State: p.remotepod.state,

View File

@ -346,8 +346,11 @@ func (c *CreateConfig) GetTmpfsMounts() []spec.Mount {
return m
}
func (c *CreateConfig) createExitCommand() []string {
config := c.Runtime.GetConfig()
func (c *CreateConfig) createExitCommand() ([]string, error) {
config, err := c.Runtime.GetConfig()
if err != nil {
return nil, err
}
cmd, _ := os.Executable()
command := []string{cmd,
@ -372,7 +375,7 @@ func (c *CreateConfig) createExitCommand() []string {
command = append(command, "--rm")
}
return command
return command, nil
}
// GetContainerCreateOptions takes a CreateConfig and returns a slice of CtrCreateOptions
@ -567,7 +570,11 @@ func (c *CreateConfig) GetContainerCreateOptions(runtime *libpod.Runtime, pod *l
}
// Always use a cleanup process to clean up Podman after termination
options = append(options, libpod.WithExitCommand(c.createExitCommand()))
exitCmd, err := c.createExitCommand()
if err != nil {
return nil, err
}
options = append(options, libpod.WithExitCommand(exitCmd))
if c.HealthCheck != nil {
options = append(options, libpod.WithHealthCheck(c.HealthCheck))

View File

@ -22,7 +22,10 @@ import (
// CreateContainer ...
func (i *LibpodAPI) CreateContainer(call iopodman.VarlinkCall, config iopodman.Create) error {
rtc := i.Runtime.GetConfig()
rtc, err := i.Runtime.GetConfig()
if err != nil {
return call.ReplyErrorOccurred(err.Error())
}
ctx := getContext()
newImage, err := i.Runtime.ImageRuntime().New(ctx, config.Image, rtc.SignaturePolicyPath, "", os.Stderr, nil, image.SigningOptions{}, false, nil)

View File

@ -514,7 +514,11 @@ func (i *LibpodAPI) Commit(call iopodman.VarlinkCall, name, imageName string, ch
if err != nil {
return call.ReplyContainerNotFound(name, err.Error())
}
sc := image.GetSystemContext(i.Runtime.GetConfig().SignaturePolicyPath, "", false)
rtc, err := i.Runtime.GetConfig()
if err != nil {
return call.ReplyErrorOccurred(err.Error())
}
sc := image.GetSystemContext(rtc.SignaturePolicyPath, "", false)
var mimeType string
switch manifestType {
case "oci", "": //nolint
@ -525,7 +529,7 @@ func (i *LibpodAPI) Commit(call iopodman.VarlinkCall, name, imageName string, ch
return call.ReplyErrorOccurred(fmt.Sprintf("unrecognized image format %q", manifestType))
}
coptions := buildah.CommitOptions{
SignaturePolicyPath: i.Runtime.GetConfig().SignaturePolicyPath,
SignaturePolicyPath: rtc.SignaturePolicyPath,
ReportWriter: nil,
SystemContext: sc,
PreferredManifestType: mimeType,

View File

@ -73,7 +73,6 @@ github.com/syndtr/gocapability d98352740cb2c55f81556b63d4a1ec64c5a319c2
github.com/tchap/go-patricia v2.2.6
github.com/uber/jaeger-client-go 64f57863bf63d3842dbe79cdc793d57baaff9ab5
github.com/uber/jaeger-lib d036253de8f5b698150d81b922486f1e8e7628ec
github.com/ulule/deepcopier ca99b135e50f526fde9cd88705f0ff2f3f95b77c
github.com/vbatts/tar-split v0.11.1
github.com/vishvananda/netlink v1.0.0
github.com/vishvananda/netns 13995c7128ccc8e51e9a6bd2b551020a27180abd

View File

@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 Ulule
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,129 +0,0 @@
# Deepcopier
[![Build Status](https://secure.travis-ci.org/ulule/deepcopier.png?branch=master)](http://travis-ci.org/ulule/deepcopier)
This package is meant to make copying of structs to/from others structs a bit easier.
## Installation
```bash
go get -u github.com/ulule/deepcopier
```
## Usage
```golang
// Deep copy instance1 into instance2
Copy(instance1).To(instance2)
// Deep copy instance1 into instance2 and passes the following context (which
// is basically a map[string]interface{}) as first argument
// to methods of instance2 that defined the struct tag "context".
Copy(instance1).WithContext(map[string]interface{}{"foo": "bar"}).To(instance2)
// Deep copy instance2 into instance1
Copy(instance1).From(instance2)
// Deep copy instance2 into instance1 and passes the following context (which
// is basically a map[string]interface{}) as first argument
// to methods of instance1 that defined the struct tag "context".
Copy(instance1).WithContext(map[string]interface{}{"foo": "bar"}).From(instance2)
```
Available options for `deepcopier` struct tag:
| Option | Description |
| --------- | -------------------------------------------------------------------- |
| `field` | Field or method name in source instance |
| `skip` | Ignores the field |
| `context` | Takes a `map[string]interface{}` as first argument (for methods) |
| `force` | Set the value of a `sql.Null*` field (instead of copying the struct) |
**Options example:**
```golang
type Source struct {
Name string
SkipMe string
SQLNullStringToSQLNullString sql.NullString
SQLNullStringToString sql.NullString
}
func (Source) MethodThatTakesContext(c map[string]interface{}) string {
return "whatever"
}
type Destination struct {
FieldWithAnotherNameInSource string `deepcopier:"field:Name"`
SkipMe string `deepcopier:"skip"`
MethodThatTakesContext string `deepcopier:"context"`
SQLNullStringToSQLNullString sql.NullString
SQLNullStringToString string `deepcopier:"force"`
}
```
Example:
```golang
package main
import (
"fmt"
"github.com/ulule/deepcopier"
)
// Model
type User struct {
// Basic string field
Name string
// Deepcopier supports https://golang.org/pkg/database/sql/driver/#Valuer
Email sql.NullString
}
func (u *User) MethodThatTakesContext(ctx map[string]interface{}) string {
// do whatever you want
return "hello from this method"
}
// Resource
type UserResource struct {
DisplayName string `deepcopier:"field:Name"`
SkipMe string `deepcopier:"skip"`
MethodThatTakesContext string `deepcopier:"context"`
Email string `deepcopier:"force"`
}
func main() {
user := &User{
Name: "gilles",
Email: sql.NullString{
Valid: true,
String: "gilles@example.com",
},
}
resource := &UserResource{}
deepcopier.Copy(user).To(resource)
fmt.Println(resource.DisplayName)
fmt.Println(resource.Email)
}
```
Looking for more information about the usage?
We wrote [an introduction article](https://github.com/ulule/deepcopier/blob/master/examples/rest-usage/README.rst).
Have a look and feel free to give us your feedback.
## Contributing
* Ping us on twitter [@oibafsellig](https://twitter.com/oibafsellig), [@thoas](https://twitter.com/thoas)
* Fork the [project](https://github.com/ulule/deepcopier)
* Help us improving and fixing [issues](https://github.com/ulule/deepcopier/issues)
Don't hesitate ;)

View File

@ -1,362 +0,0 @@
package deepcopier
import (
"database/sql/driver"
"fmt"
"reflect"
"strings"
)
const (
// TagName is the deepcopier struct tag name.
TagName = "deepcopier"
// FieldOptionName is the from field option name for struct tag.
FieldOptionName = "field"
// ContextOptionName is the context option name for struct tag.
ContextOptionName = "context"
// SkipOptionName is the skip option name for struct tag.
SkipOptionName = "skip"
// ForceOptionName is the skip option name for struct tag.
ForceOptionName = "force"
)
type (
// TagOptions is a map that contains extracted struct tag options.
TagOptions map[string]string
// Options are copier options.
Options struct {
// Context given to WithContext() method.
Context map[string]interface{}
// Reversed reverses struct tag checkings.
Reversed bool
}
)
// DeepCopier deep copies a struct to/from a struct.
type DeepCopier struct {
dst interface{}
src interface{}
ctx map[string]interface{}
}
// Copy sets source or destination.
func Copy(src interface{}) *DeepCopier {
return &DeepCopier{src: src}
}
// WithContext injects the given context into the builder instance.
func (dc *DeepCopier) WithContext(ctx map[string]interface{}) *DeepCopier {
dc.ctx = ctx
return dc
}
// To sets the destination.
func (dc *DeepCopier) To(dst interface{}) error {
dc.dst = dst
return process(dc.dst, dc.src, Options{Context: dc.ctx})
}
// From sets the given the source as destination and destination as source.
func (dc *DeepCopier) From(src interface{}) error {
dc.dst = dc.src
dc.src = src
return process(dc.dst, dc.src, Options{Context: dc.ctx, Reversed: true})
}
// process handles copy.
func process(dst interface{}, src interface{}, args ...Options) error {
var (
options = Options{}
srcValue = reflect.Indirect(reflect.ValueOf(src))
dstValue = reflect.Indirect(reflect.ValueOf(dst))
srcFieldNames = getFieldNames(src)
srcMethodNames = getMethodNames(src)
)
if len(args) > 0 {
options = args[0]
}
if !dstValue.CanAddr() {
return fmt.Errorf("destination %+v is unaddressable", dstValue.Interface())
}
for _, f := range srcFieldNames {
var (
srcFieldValue = srcValue.FieldByName(f)
srcFieldType, srcFieldFound = srcValue.Type().FieldByName(f)
srcFieldName = srcFieldType.Name
dstFieldName = srcFieldName
tagOptions TagOptions
)
if !srcFieldFound {
continue
}
if options.Reversed {
tagOptions = getTagOptions(srcFieldType.Tag.Get(TagName))
if v, ok := tagOptions[FieldOptionName]; ok && v != "" {
dstFieldName = v
}
} else {
if name, opts := getRelatedField(dst, srcFieldName); name != "" {
dstFieldName, tagOptions = name, opts
}
}
if _, ok := tagOptions[SkipOptionName]; ok {
continue
}
var (
dstFieldType, dstFieldFound = dstValue.Type().FieldByName(dstFieldName)
dstFieldValue = dstValue.FieldByName(dstFieldName)
)
if !dstFieldFound {
continue
}
// Force option for empty interfaces and nullable types
_, force := tagOptions[ForceOptionName]
// Valuer -> ptr
if isNullableType(srcFieldType.Type) && dstFieldValue.Kind() == reflect.Ptr && force {
// We have same nullable type on both sides
if srcFieldValue.Type().AssignableTo(dstFieldType.Type) {
dstFieldValue.Set(srcFieldValue)
continue
}
v, _ := srcFieldValue.Interface().(driver.Valuer).Value()
if v == nil {
continue
}
valueType := reflect.TypeOf(v)
ptr := reflect.New(valueType)
ptr.Elem().Set(reflect.ValueOf(v))
if valueType.AssignableTo(dstFieldType.Type.Elem()) {
dstFieldValue.Set(ptr)
}
continue
}
// Valuer -> value
if isNullableType(srcFieldType.Type) {
// We have same nullable type on both sides
if srcFieldValue.Type().AssignableTo(dstFieldType.Type) {
dstFieldValue.Set(srcFieldValue)
continue
}
if force {
v, _ := srcFieldValue.Interface().(driver.Valuer).Value()
if v == nil {
continue
}
rv := reflect.ValueOf(v)
if rv.Type().AssignableTo(dstFieldType.Type) {
dstFieldValue.Set(rv)
}
}
continue
}
if dstFieldValue.Kind() == reflect.Interface {
if force {
dstFieldValue.Set(srcFieldValue)
}
continue
}
// Ptr -> Value
if srcFieldType.Type.Kind() == reflect.Ptr && !srcFieldValue.IsNil() && dstFieldType.Type.Kind() != reflect.Ptr {
indirect := reflect.Indirect(srcFieldValue)
if indirect.Type().AssignableTo(dstFieldType.Type) {
dstFieldValue.Set(indirect)
continue
}
}
// Other types
if srcFieldType.Type.AssignableTo(dstFieldType.Type) {
dstFieldValue.Set(srcFieldValue)
}
}
for _, m := range srcMethodNames {
name, opts := getRelatedField(dst, m)
if name == "" {
continue
}
if _, ok := opts[SkipOptionName]; ok {
continue
}
method := reflect.ValueOf(src).MethodByName(m)
if !method.IsValid() {
return fmt.Errorf("method %s is invalid", m)
}
var (
dstFieldType, _ = dstValue.Type().FieldByName(name)
dstFieldValue = dstValue.FieldByName(name)
_, withContext = opts[ContextOptionName]
_, force = opts[ForceOptionName]
)
args := []reflect.Value{}
if withContext {
args = []reflect.Value{reflect.ValueOf(options.Context)}
}
var (
result = method.Call(args)[0]
resultInterface = result.Interface()
resultValue = reflect.ValueOf(resultInterface)
resultType = resultValue.Type()
)
// Value -> Ptr
if dstFieldValue.Kind() == reflect.Ptr && force {
ptr := reflect.New(resultType)
ptr.Elem().Set(resultValue)
if ptr.Type().AssignableTo(dstFieldType.Type) {
dstFieldValue.Set(ptr)
}
continue
}
// Ptr -> value
if resultValue.Kind() == reflect.Ptr && force {
if resultValue.Elem().Type().AssignableTo(dstFieldType.Type) {
dstFieldValue.Set(resultValue.Elem())
}
continue
}
if resultType.AssignableTo(dstFieldType.Type) && result.IsValid() {
dstFieldValue.Set(result)
}
}
return nil
}
// getTagOptions parses deepcopier tag field and returns options.
func getTagOptions(value string) TagOptions {
options := TagOptions{}
for _, opt := range strings.Split(value, ";") {
o := strings.Split(opt, ":")
// deepcopier:"keyword; without; value;"
if len(o) == 1 {
options[o[0]] = ""
}
// deepcopier:"key:value; anotherkey:anothervalue"
if len(o) == 2 {
options[strings.TrimSpace(o[0])] = strings.TrimSpace(o[1])
}
}
return options
}
// getRelatedField returns first matching field.
func getRelatedField(instance interface{}, name string) (string, TagOptions) {
var (
value = reflect.Indirect(reflect.ValueOf(instance))
fieldName string
tagOptions TagOptions
)
for i := 0; i < value.NumField(); i++ {
var (
vField = value.Field(i)
tField = value.Type().Field(i)
tagOptions = getTagOptions(tField.Tag.Get(TagName))
)
if tField.Type.Kind() == reflect.Struct && tField.Anonymous {
if n, o := getRelatedField(vField.Interface(), name); n != "" {
return n, o
}
}
if v, ok := tagOptions[FieldOptionName]; ok && v == name {
return tField.Name, tagOptions
}
if tField.Name == name {
return tField.Name, tagOptions
}
}
return fieldName, tagOptions
}
// getMethodNames returns instance's method names.
func getMethodNames(instance interface{}) []string {
var methods []string
t := reflect.TypeOf(instance)
for i := 0; i < t.NumMethod(); i++ {
methods = append(methods, t.Method(i).Name)
}
return methods
}
// getFieldNames returns instance's field names.
func getFieldNames(instance interface{}) []string {
var (
fields []string
v = reflect.Indirect(reflect.ValueOf(instance))
t = v.Type()
)
if t.Kind() != reflect.Struct {
return nil
}
for i := 0; i < v.NumField(); i++ {
var (
vField = v.Field(i)
tField = v.Type().Field(i)
)
// Is exportable?
if tField.PkgPath != "" {
continue
}
if tField.Type.Kind() == reflect.Struct && tField.Anonymous {
fields = append(fields, getFieldNames(vField.Interface())...)
continue
}
fields = append(fields, tField.Name)
}
return fields
}
// isNullableType returns true if the given type is a nullable one.
func isNullableType(t reflect.Type) bool {
return t.ConvertibleTo(reflect.TypeOf((*driver.Valuer)(nil)).Elem())
}