smapping

package module
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Oct 30, 2020 License: MIT Imports: 4 Imported by: 150

README

license CircleCI GoDoc

smapping

Golang structs generic mapping.

Version Limit

To support nesting object conversion, the lowest Golang version supported is 1.12.0.

Motivation

Working with between struct, and json with Golang has various degree of difficulty. The thing that makes difficult is that sometimes we get arbitrary json or have to make json with arbitrary fields.
In Golang we can achieve that using interface{} that act as "Any" value. While it seems good, however interface{} only gives us ability to disable type propagation and various reasoning. It's a trade-off that one has to make to enable interfacing with dynamically defined such as json.
It's now able to map and fill nested objects.

Install

go get github.com/mashingan/smapping

Example

package main

import (
	"encoding/json"
	"fmt"

	"github.com/mashingan/smapping"
)

type Source struct {
	Label   string `json:"label"`
	Info    string `json:"info"`
	Version int    `json:"version"`
}

type Sink struct {
	Label string
	Info  string
}

type HereticSink struct {
	NahLabel string `json:"label"`
	HahaInfo string `json:"info"`
	Version  string `json:"heretic_version"`
}

type DifferentOneField struct {
	Name    string `json:"name"`
	Label   string `json:"label"`
	Code    string `json:"code"`
	Private string `json:"private" api:"internal"`
}

func main() {
	source := Source{
		Label:   "source",
		Info:    "the origin",
		Version: 1,
	}
	fmt.Println("source:", source)
	mapped := smapping.MapFields(&source)
	fmt.Println("mapped:", mapped)
	sink := Sink{}
	err := smapping.FillStruct(&sink, mapped)
	if err != nil {
		panic(err)
	}
	fmt.Println("sink:", sink)

	maptags := smapping.MapTags(&source, "json")
	fmt.Println("maptags:", maptags)
	hereticsink := HereticSink{}
	err = smapping.FillStructByTags(&hereticsink, maptags, "json")
	if err != nil {
		panic(err)
	}
	fmt.Println("heretic sink:", hereticsink)

	fmt.Println("=============")
	recvjson := []byte(`{"name": "bella", "label": "balle", "code": "albel", "private": "allbe"}`)
	dof := DifferentOneField{}
	_ = json.Unmarshal(recvjson, &dof)
	fmt.Println("unmarshaled struct:", dof)

	marshaljson, _ := json.Marshal(dof)
	fmt.Println("marshal back:", string(marshaljson))

	// What we want actually "internal" instead of "private" field
	// we use the api tags on to make the json
	apijson, _ := json.Marshal(smapping.MapTagsWithDefault(&dof, "api", "json"))
	fmt.Println("api marshal:", string(apijson))

	fmt.Println("=============")
	// This time is the reverse, we receive "internal" field when
	// we need to receive "private" field to match our json tag field
	respjson := []byte(`{"name": "bella", "label": "balle", "code": "albel", "internal": "allbe"}`)
	respdof := DifferentOneField{}
	_ = json.Unmarshal(respjson, &respdof)
	fmt.Println("unmarshal resp:", respdof)

	// to get that, we should put convert the json to Mapped first
	jsonmapped := smapping.Mapped{}
	_ = json.Unmarshal(respjson, &jsonmapped)
	// now we fill our struct respdof
	_ = smapping.FillStructByTags(&respdof, jsonmapped, "api")
	fmt.Println("full resp:", respdof)
	returnback, _ := json.Marshal(respdof)
	fmt.Println("marshal resp back:", string(returnback))
	// first we unmarshal respdof, we didn't get the "private" field
	// but after our mapping, we get "internal" field value and
	// simply marshaling back to `returnback`
}

Nested object example


type RefLevel3 struct {
	What string `json:"finally"`
}
type Level2 struct {
	*RefLevel3 `json:"ref_level3"`
}
type Level1 struct {
	Level2 `json:"level2"`
}
type TopLayer struct {
	Level1 `json:"level1"`
}
type MadNest struct {
	TopLayer `json:"top"`
}

var madnestStruct MadNest = MadNest{
	TopLayer: TopLayer{
		Level1: Level1{
			Level2: Level2{
				RefLevel3: &RefLevel3{
					What: "matryoska",
				},
			},
		},
	},
}

func main() {
	// since we're targeting the same MadNest, both of functions will yield
	// same result hence this unified example/test.
	var madnestObj MadNest
	var err error
	testByTags := true
	if testByTags {
		madnestMap := smapping.MapTags(&madnestStruct, "json")
		err = smapping.FillStructByTags(&madnestObj, madnestMap, "json")
	} else {
		madnestMap := smapping.MapFields(&madnestStruct)
		err = smapping.FillStruct(&madnestObj)
	}
	if err != nil {
		fmt.Printf("%s", err.Error())
		return
	}
	// the result should yield as intented value.
	if madnestObj.TopLayer.Level1.Level2.RefLevel3.What != "matryoska" {
		fmt.Printf("Error: expected \"matroska\" got \"%s\"", madnestObj.Level1.Level2.RefLevel3.What)
	}
}

LICENSE

MIT

Documentation

Overview

Package smapping is Library for collecting various operations on struct and its mapping to interface{} and/or map[string]interface{} type. Implemented to ease the conversion between Golang struct and json format together with ease of mapping selections using different part of field tagging.

The implementation is abstraction on top reflection package, reflect.

Examples

The snippet code below will be used accross example for brevity

    type source struct {
	Label   string `json:"label"`
	Info    string `json:"info"`
	Version int    `json:"version"`
    }

    type sink struct {
	Label string	// note that we don't include struct tag
	Info  string
    }

    type differentSink struct {
	DiffLabel string `json:"label"`	    // note that this struct
	NiceInfo  string `json:"info"`	    // has different field name
	Version   string `json:"unversion"` // but same json tag
    }

    var sourceobj source = source{
	Label:   "source",
	Info:    "the origin",
	Version: 1,
    }

    func printIfNotExists(mapped Mapped, keys ...string) {
	for _, key := range keys {
	    if _, ok := mapped[key]; !ok {
		fmt.Println(false)
	    }
	}
    }

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func FillStruct

func FillStruct(obj interface{}, mapped Mapped) error

FillStruct acts just like “json.Unmarshal“ but works with “Mapped“ instead of bytes of char that made from “json“.

Example
mapped := MapFields(&sourceobj)
sinked := sink{}
err := FillStruct(&sinked, mapped)
if err != nil {
	panic(err)
}
fmt.Println(sinked)
Output:
{source the origin}

func FillStructByTags

func FillStructByTags(obj interface{}, mapped Mapped, tagname string) error

FillStructByTags fills the field that has tagname and tagvalue instead of Mapped key name.

Example
maptags := MapTags(&sourceobj, "json")
for k, v := range maptags {
	fmt.Printf("maptags[%s]: %v\n", k, v)
}
diffsink := differentSink{}
err := FillStructByTags(&diffsink, maptags, "json")
if err != nil {
	panic(err)
}
fmt.Println(diffsink)
Output:
maptags[label]: source
maptags[info]: the origin
maptags[version]: 1
{source the origin }

Types

type Mapped

type Mapped map[string]interface{}

Mapped simply an alias

func MapFields

func MapFields(x interface{}) Mapped

MapFields maps between struct to mapped interfaces{}. The argument must be pointer struct or else it will throw panic error.

Only map the exported fields.

Example
mapped := MapFields(&sourceobj)
printIfNotExists(mapped, "Label", "Info", "Version")

func MapTags

func MapTags(x interface{}, tag string) Mapped

MapTags maps the tag value of defined field tag name. This enable various field extraction that will be mapped to mapped interfaces{}.

Example (Basic)
maptags := MapTags(&sourceobj, "json")
printIfNotExists(maptags, "label", "info", "version")
Example (Nested)
nestedSource := differentSourceSink{
	Source: sourceobj,
	DiffSink: differentSink{
		DiffLabel: "nested diff",
		NiceInfo:  "nested info",
		Version:   "next version",
	},
}
nestedMap := MapTags(&nestedSource, "json")
for k, v := range nestedMap {
	fmt.Println("top key:", k)
	for kk, vv := range v.(Mapped) {
		fmt.Println("    nested:", kk, vv)
	}
	fmt.Println()
}
Output:
top key: source
    nested: label source
    nested: info the origin
    nested: version 1

top key: differentSink
    nested: label nested diff
    nested: info nested info
    nested: unversion next version
Example (TwoTags)
general := generalFields{
	Name:     "duran",
	Rank:     "private",
	Code:     1337,
	nickname: "drone",
}
mapjson := MapTags(&general, "json")
printIfNotExists(mapjson, "name", "rank", "code")

mapapi := MapTags(&general, "api")
printIfNotExists(mapapi, "general_name", "general_rank", "general_code")

func MapTagsFlatten added in v0.1.2

func MapTagsFlatten(x interface{}, tag string) Mapped

MapTagsFlatten is to flatten mapped object with specific tag. The limitation of this flattening that it can't have duplicate tag name and it will give incorrect result because the older value will be written with newer map field value.

Example
type (
	Last struct {
		Final string `json:"final"`
	}
	Lv3 struct {
		Lv3Str string `json:"lv3str"`
		*Last
	}
	Lv2 struct {
		Lv2Str string `json:"lv2str"`
		Lv3
	}
	Lv1 struct {
		Lv2
		Lv1Str string `json:"lv1str"`
	}
)

obj := Lv1{
	Lv1Str: "level 1 string",
	Lv2: Lv2{
		Lv2Str: "level 2 string",
		Lv3: Lv3{
			Lv3Str: "level 3 string",
			Last: &Last{
				Final: "destination",
			},
		},
	},
}

for k, v := range MapTagsFlatten(&obj, "json") {
	fmt.Printf("key: %s, value: %v\n", k, v)
}
Output:
key: final, value: destination
key: lv1str, value: level 1 string
key: lv2str, value: level 2 string
key: lv3str, value: level 3 string

func MapTagsWithDefault

func MapTagsWithDefault(x interface{}, tag string, defs ...string) Mapped

MapTagsWithDefault maps the tag with optional fallback tags. This to enable tag differences when there are only few difference with the default “json“ tag.

Example
type higherCommon struct {
	General     generalFields `json:"general"`
	Communality string        `json:"common"`
	Available   bool          `json:"available" api:"is_available"`
}
rawjson := []byte(`{
	    "general": {
		name:     "duran",
		rank:     "private",
		code:     1337,
	    },
	    "common": "rare",
	    "available": true
	}`)
hc := higherCommon{}
_ = json.Unmarshal(rawjson, &hc)
maptags := MapTagsWithDefault(&hc, "api", "json")
printIfNotExists(maptags, "available")
Output:
available : not exists

Jump to

Keyboard shortcuts

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