~/snippets/go-struct-to-opensearch-mapping
Published on

Go struct to OpenSearch mapping

1766 words9 min read
package main

import (
	"fmt"
	"reflect"
	"regexp"
	"strings"
)

type Time struct {
	Time string
}

type DoubleNestedThing struct {
	DoubleNestedA string  `json:"doubleNestedA,omitempty"`
	DoubleNestedB *string `json:"doubleNestedB,omitempty"`
	DoubleNestedC *bool   `json:"doubleNestedC,omitempty"`
}

type InlineThing struct {
	InlineA string `json:"inlineA,omitempty"`
	InlineB string `json:"inlineB,omitempty"`
}

type SliceThing struct {
	SliceA string            `json:"sliceA,omitempty"`
	SliceB int               `json:"sliceB,omitempty"`
	SliceC map[string]string `json:"sliceC,omitempty"`
}

type NestedThing struct {
	NestedA string            `json:"nestedA,omitempty"`
	NestedB []string          `json:"nestedB,omitempty"`
	NestedC DoubleNestedThing `json:"nestedC,omitempty"`
	NestedD []SliceThing      `json:"nestedD,omitempty"`
}

type PointerThing struct {
	PointerA *string  `json:"pointerA,omitempty"`
	PointerB *bool    `json:"pointerB,omitempty"`
	PointerC *int     `json:"pointerC,omitempty"`
	PointerD *float32 `json:"pointerD,omitempty"`
}

type RepeatedThing struct {
	RepeatedA string `json:"repeatedA,omitempty"`
	RepeatedB string `json:"repeatedB,omitempty"`
}

type AppReleaseType string

const (
	AppReleaseTypeUnreleased AppReleaseType = "Unreleased"
	AppReleaseTypeReleased   AppReleaseType = "Released"
)

type AppConfig struct {
	ConfFile string `json:"conf,omitempty"`
	Stanza   string `json:"stanza,omitempty"`
	Key      string `json:"key,omitempty"`
	Value    string `json:"value,omitempty"`
}

type App struct {
	ID                     int32               `json:"id,omitempty"`
	Targets                []string            `json:"targets,omitempty"`
	Version                string              `json:"version,omitempty"`
	Name                   string              `json:"name,omitempty"`
	PurgeConf              map[string][]string `json:"purgeconf,omitempty"`
	PurgeDir               map[string][]string `json:"purgedir,omitempty"`
	Config                 []AppConfig         `json:"config,omitempty"`
	Ensure                 string              `json:"ensure,omitempty"`
	SkipCompatibilityCheck bool                `json:"skipCompatibilityCheck,omitempty"`
	ExtractPath            string              `json:"extractPath,omitempty"`
	Context                *string             `json:"context,omitempty"`
	ReleaseType            AppReleaseType      `json:"releaseType,omitempty"`
}

type ThisThing struct {
	InlineThing   `json:",inline"`
	AField        string              `json:"aField,omitempty"`
	BField        map[string][]string `json:"bField,omitempty"`
	CField        NestedThing         `json:"cField,omitempty"`
	DField        int                 `json:"dField,omitempty"`
	EField        int8                `json:"eField,omitempty"`
	FField        int16               `json:"fField,omitempty"`
	GField        int32               `json:"gField,omitempty"`
	HField        int64               `json:"hField,omitempty"`
	IField        bool                `json:"iField,omitempty"`
	Time          Time                `json:"time,omitempty"`
	PointerThing  *PointerThing       `json:"pointerThing,omitempty"`
	RepeatedField []RepeatedThing     `json:"repeatedField,omitempty"`
	Apps          []App               `json:"apps,omitempty"`
}

/*

Open Search Types
===================
* Null
* Boolean
* Float
* Double
* Integer
* Object
* Array
* Text
* Keyword
* Date Detection String
* Numeric Detection String

{
  "mappings": {
    "properties": {
      "year":       { "type": "text" },
      "age":        {"type": "integer"},
      "director":   {"type": "text"}
    }
  }
}

*/

func main() {
	tt := &ThisThing{}
	// tt.BuildMapping()
	fmt.Println(string(tt.BuildMapping()))
}

func (u *ThisThing) BuildMapping() []byte {
	value := reflect.ValueOf(*u)
	t := value.Type()

	return buildMapping(t, false, false)
}

func handleMappingKind(k reflect.Kind) []byte {
	out := make([]byte, 0)

	switch k {
	case reflect.Map:
		return append(out, []byte(`{"type": "object"}`)...)
	case reflect.Bool:
		return append(out, []byte(`{"type": "boolean"}`)...)
	case reflect.Float32:
		return append(out, []byte(`{"type": "float"}`)...)
	case reflect.Float64:
		return append(out, []byte(`{"type": "double"}`)...)
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		return append(out, []byte(`{"type": "integer"}`)...)
	}

	return append(out, []byte(`{"type": "text"}`)...)
}

func handleMappingType(t reflect.Type, i int, tag string) []byte {
	var out []byte

	// Handle custom types differently if needed
	switch t.Field(i).Type {
	case reflect.TypeOf(Time{}):
		return append(out, []byte(`{"type": "text"}`)...)
	}

	switch t.Field(i).Type.Kind() {
	case reflect.Slice:
		if t.Field(i).Type.Elem().Kind() == reflect.Struct {
			return append(out, buildMapping(t.Field(i).Type.Elem(), true, tag == ",inline")...)
		}
		return append(out, []byte(`{"type": "keyword"}`)...)
	case reflect.Struct:
		return append(out, buildMapping(t.Field(i).Type, true, tag == ",inline")...)
	case reflect.Pointer:
		if t.Field(i).Type.Elem().Kind() == reflect.Struct {
			return append(out, buildMapping(t.Field(i).Type.Elem(), true, tag == ",inline")...)
		}

		return append(out, handleMappingKind(t.Field(i).Type.Elem().Kind())...)
	}

	return append(out, handleMappingKind(t.Field(i).Type.Kind())...)
}

func buildMapping(t reflect.Type, isRecursive bool, isInline bool) []byte {
	var out []byte

	if !isInline {
		out = append(out, '{')
	}

	if !isInline && isRecursive {
		out = append(out, []byte(`"type": "object",`)...)
	}

	if !isInline {
		out = append(out, []byte(`"properties": {`)...)
	}

	r := regexp.MustCompile(`json:"(?P<tag>.*?)"`)

	for i := 0; i < t.NumField(); i++ {
		m := r.FindStringSubmatch(string(t.Field(i).Tag))
		if len(m) == 0 {
			continue
		}

		tag := strings.ReplaceAll(m[r.SubexpIndex("tag")], ",omitempty", "")

		if tag != ",inline" {
			out = append(out, fmt.Sprintf(`"%s": `, tag)...)
		}

		out = append(out, handleMappingType(t, i, tag)...)

		if i < t.NumField()-1 {
			out = append(out, ',')
		}
	}

	if len(out) > 0 && out[len(out)-1] == 44 {
		out = out[:len(out)-1]
	}

	if !isInline {
		out = append(out, []byte("}}")...)
	}

	return out
}

References