Skip to content

A golang package to create, modify and generate structs dynamically at runtime.

License

Notifications You must be signed in to change notification settings

MartinSimango/dstruct

Repository files navigation

⚠️ Notice

  • Please do not install any of these versions: v1.1.1 v1.1.0 v1.0.0 v0.1.2 v0.1.1 as these were removed from the repo - (but are still available at pkg.go.dev).
  • When installing please explicitly install the actual latest version of dstruct which is currently v0.3.0-beta.

dstruct

A golang package that allows one to create, modify and generate structs dynamically.

Features:

  • Building structs at runtime
  • Extending existing struct at runtime
  • Merging multiple structs at runtime
  • Adding new fields into struct
  • Removing existing fields from struct
  • Modifying field values in structs
  • Reading field values in structs
  • Generating struct values

Limitations:

  • You cannot extend structs with unexported embedded fields.
  • If a struct pointer cannot be fully dereferenced then the struct's subfields won't be added to the dynamic struct. This is done mainly to avoid self-referencing structs as these will create infinite node trees.
  • Dynamic structs with struct fields of type any (interface {}) cannot be created. If you try extend or merge structs which have struct fields of type any their value must be set to a concrete type.

Sections

Install

go get github.com/MartinSimango/dstruct@v0.3.0-beta

How it works?

Dstruct uses a tree to represent dynamic structs which allows these structs to easily to be manipulated. Nodes and their children represent struct fields and their subfields respectively. Once the tree structure of the struct is created the tree is converted into a dynamic struct using the reflect package.

Dstruct has 3 main interfaces that are implemented in order to allow these features:

  1. dstruct.Builder is responsible for adding and removing fields from a struct.
type Builder interface {
    AddField(name string, value interface{}, tag string) Builder
    AddEmbeddedField(value interface{}, tag string) Builder
    Build() DynamicStructModifier
    GetField(name string) Builder
    GetFieldCopy(field string) Builder
    NewBuilderFromField(field string) Builder
    RemoveField(name string) Builder
}
  1. dstruct.DynamicStructModifier is responsible for reading and editing fields with the struct as well as storing the actual struct.
type DynamicStructModifier interface {
    Instance() any
    New() any
    Get(field string) (any, error)
    Set(field string, value any) error
    GetFields() map[string]field
}
  1. dstruct.GeneratedStruct is responsible for generating struct values and is an extension of the DynamicStructModifier. A generated struct values are randomly generation based on Generation functions.
type GeneratedStruct interface {
    DynamicStructModifier
    Generate()
    GetFieldGenerationConfig(field string) *generator.GenerationConfig
    SetFieldGenerationConfig(field string, generationConfig *generator.GenerationConfig) error
}

Using the Builder

type Person struct {
	Name string
	Age  int
}

func main() {
	structBuilder := dstruct.NewBuilder().
		AddField("Person", Person{Name: "Martin", Age: 25}, `json:"person"`).
		AddField("Job", "Software Developer", "").
		RemoveField("Person.Age")

	fmt.Printf("Struct: %+v\n", structBuilder.Build().Instance())
}

Output

$ Struct: {Person:{Name:Martin} Job:Software Developer}

Using the Modifier

type Person struct {
	Name string
	Age  int
}

func main() {
	structBuilder := dstruct.NewBuilder().
		AddField("Person", Person{Name: "Martin", Age: 25}, `json:"person"`).
		AddField("Job", "Software Developer", "")

	structModifier := structBuilder.Build()
	structModifier.Set("Person.Name", "Martin Simango")
	structModifier.Set("Job", "Software Engineer")

	name, _ := structModifier.Get("Person.Name")

	fmt.Printf("New name: %s\n", name.(string))
	fmt.Printf("Struct: %+v\n", structModifier.Instance())
}

Output

$ New name: Martin Simango
$ Struct: {Person:{Name:Martin Simango Age:25} Job:Software Engineer}

Using the Struct Generator

type Person struct {
	Name string
	Age  int
}

func main() {
	structBuilder := dstruct.NewBuilder().
		AddField("Person", Person{Name: "Martin", Age: 25}, `json:"person"`).
		AddField("Job", "Software Developer", "")

		

	strct := structBuilder.Build().Instance()

	generatedStruct := dstruct.NewGeneratedStruct(strct)
	// change the age to be between 50 and 60
	generatedStruct.GetFieldGenerationConfig("Person.Age").SetIntMin(50).SetIntMax(60)
	generatedStruct.Generate()
	fmt.Printf("Struct with age between 50 and 60: %+v\n", generatedStruct.Instance())
	// change the age to be between 10 and 20
	generatedStruct.GetFieldGenerationConfig("Person.Age").SetIntMin(10).SetIntMax(20)
	generatedStruct.Generate()
	fmt.Printf("Struct with age between 10 and 20: %+v\n", generatedStruct.Instance())

}

Output:

$ Struct with age between 50 and 60: {Person:{Name:string Age:59} Job:string}
$ Struct with age between 10 and 20: {Person:{Name:string Age:16} Job:string}

Extending a struct

type Address struct {
	Street string
}

type Person struct {
	Name    string
	Age     int
	Address Address
}

func main() {
	strct := dstruct.ExtendStruct(Person{
		Name: "Martin",
		Age:  25,
		Address: Address{
			Street: "Alice Street",
		},
	})
	strct.GetField("Address").AddField("StreetNumber", 1, "")

	fmt.Printf("Extended struct: %+v\n", strct.Build().Instance())

}

Output:

$ Extended struct: {Name:Martin Age:25 Address:{Street:Alice Street StreetNumber:1}}

Merging structs

type PersonDetails struct {
	Age    int
	Height float64
}

type PersonName struct {
	Name    string
	Surname string
}

func main() {
	strct, _ := dstruct.MergeStructs(PersonDetails{Age: 0, Height: 190.4}, PersonName{Name: "Martin", Surname: "Simango"})
	fmt.Printf("Merged structs: %+v\n", strct)
}

Output:

$ Merged structs: {Age:0 Height:190.4 Name:Martin Surname:Simango}