Skip to content

Instantly share code, notes, and snippets.

@beeekind
Created January 1, 2021 17:31
Show Gist options
  • Save beeekind/4c25283856f774040ceef1df4e992c27 to your computer and use it in GitHub Desktop.
Save beeekind/4c25283856f774040ceef1df4e992c27 to your computer and use it in GitHub Desktop.
package json2go
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"sort"
"strings"
"text/template"
"github.com/gertd/go-pluralize"
)
var plural = pluralize.NewClient()
var structTemplate = template.Must(template.New("").Funcs(template.FuncMap{
"PrepareTags": PrepareTags,
"NotSubqueryable": NotSubqueryable,
"IsParent": func(s *StructDefinition) bool {
return s.ParentName == ""
},
}).Parse(`
{{- if . | IsParent }}
// {{.StructName}}Properties useful for building SOQL queries to the tooling/query api
var {{.StructName}}Properties = []string{
{{- range .Properties | NotSubqueryable}}
{{- if .Documentation }}
// {{$.StructName}}{{.Name}} ...
// {{.Documentation}}
"{{.Name}}",
{{- end}}
{{- end}}
}
{{- end }}
// {{.StructName}} ...
// {{.StructDocumentation}}
type {{.StructName}} struct {
{{- range .Properties}}
// {{.Name}} ...
// {{.Documentation}}
{{.Name}} {{.Type}} {{.Tags | PrepareTags}}
{{- end}}
}`))
var DefaultTypeConversionMap = map[string]string{
"bool": "bool",
"string": "string",
"float64": "float64",
"null": "json.RawMessage",
"[]": "[]interface{}",
"?": "interface{}",
}
// StructDefinition ...
type StructDefinition struct {
ParentName string
StructName string
StructDocumentation string
Properties []*Property
ChildProperties []*Property
}
// Property ...
type Property struct {
ParentName string
Name string
Documentation string
Type string
Tags []Tag
}
// Tag ...
type Tag struct {
Key string
Values []string
}
func (d *StructDefinition) String() string {
buff := bytes.NewBufferString("")
if err := structTemplate.Execute(buff, d); err != nil {
panic(fmt.Errorf("String(1): %w", err))
}
return buff.String()
}
// Convert ...
func Convert(structName string, structDocumentation string, JSON []byte) (types []string, err error) {
results, err := JSON2Go(structName, structDocumentation, JSON, DefaultTypeConversionMap)
if err != nil {
return nil, fmt.Errorf("Convert(1): %w", err)
}
for i := 0; i < len(results); i++ {
types = append(types, results[i].String())
}
return types, nil
}
// JSON2Go ...
func JSON2Go(structName string, structDocumentation string, JSON []byte, typeConversionMap map[string]string) (results []*StructDefinition, err error) {
raw := map[string]interface{}{}
if err := json.Unmarshal(JSON, &raw); err != nil {
return nil, fmt.Errorf("JSON2Go(): %w", err)
}
var properties []*Property
idx := 0
for k, v := range raw {
property := &Property{
Name: PrepareName(k, false, false),
Tags: []Tag{
{k, []string{fmt.Sprintf("json:\"%s\"", k)}},
},
}
t := reflect.TypeOf(v)
if t == nil {
property.Type = typeConversionMap["null"]
properties = append(properties, property)
idx++
continue
}
switch t.Kind().String() {
case "bool":
property.Type = typeConversionMap["bool"]
case "string":
property.Type = typeConversionMap["string"]
case "float64":
property.Type = typeConversionMap["float64"]
case "map":
property.Name = PrepareName(k, true, false)
property.Type = "*" + PrepareName(k, false, true)
contents, err := json.Marshal(v)
if err != nil {
return nil, fmt.Errorf("JSON2Go(): %w", err)
}
subStructs, err := JSON2Go(PrepareName(k, false, true), "", contents, typeConversionMap)
if err != nil {
return nil, fmt.Errorf("JSON2Go(): %w", err)
}
for i := 0; i < len(subStructs); i++ {
subStructs[i].ParentName = structName
}
results = append(results, subStructs...)
case "slice":
property.Name = PrepareName(k, true, false)
property.Type = "[]*" + PrepareName(k, false, true)
elements, ok := v.([]interface{})
if !ok {
return nil, fmt.Errorf("JSON2Go(): %s", "slice is not of []interface{}")
}
// represented as empty slice
if len(elements) == 0 {
property.Type = typeConversionMap["[]"]
properties = append(properties, property)
idx++
continue
}
item := elements[0]
m, ok := item.(map[string]interface{})
if !ok {
_, ok2 := item.(string)
if ok2 {
property.Type = "[]string"
property.Tags = []Tag{
{k, []string{fmt.Sprintf("json:\"%s\"", k)}},
}
properties = append(properties, property)
idx++
continue
}
fmt.Printf("\n\n%v\n\n", item)
return nil, fmt.Errorf("JSON2Go(): can't caste item into map[string]interface{}")
}
contents, err := json.Marshal(m)
if err != nil {
return nil, fmt.Errorf("JSON2Go(): %w", err)
}
subStructs, err := JSON2Go(PrepareName(k, false, true), "", contents, typeConversionMap)
if err != nil {
fmt.Printf("%s:%v\n", k, v)
return nil, fmt.Errorf("JSON2Go(): %w", err)
}
for i := 0; i < len(subStructs); i++ {
subStructs[i].ParentName = structName
}
results = append(results, subStructs...)
default:
if _, exists := typeConversionMap["?"]; exists {
property.Type = typeConversionMap["?"]
break
}
return nil, fmt.Errorf("JSON2Go(): could not parse type %s", t.Kind().String())
}
properties = append(properties, property)
idx++
}
var childProperties []*Property
for i := 0; i < len(results); i++ {
for j := 0; j < len(results[i].Properties); j++ {
childProperty := results[i].Properties[j]
childProperty.ParentName = results[i].StructName
childProperties = append(childProperties, childProperty)
}
}
sort.Slice(properties, func(i, j int) bool {
return properties[i].Name < properties[j].Name
})
results = append(results, &StructDefinition{
StructName: structName,
StructDocumentation: structDocumentation,
Properties: properties,
ChildProperties: childProperties,
})
return results, nil
}
// PrepareName ...
func PrepareName(propertyKey string, isPlural bool, isSingular bool) string {
if isPlural {
propertyKey = plural.Plural(propertyKey)
}
if isSingular {
propertyKey = plural.Singular(propertyKey)
}
parts := strings.Split(propertyKey, ",")
if len(parts) > 1 {
propertyKey = parts[len(parts)-1]
}
result := ConvertInitialisms(strings.Title(propertyKey))
return strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(result, " ", ""), "-", ""), ".", "")
}
// PrepareTags ...
func PrepareTags(tags []Tag) string {
var items []string
for _, tag := range tags {
items = append(items, strings.Join(tag.Values, ","))
}
return "`" + strings.Join(items, " ") + "`"
}
func SimpleCompose(definitions ...*StructDefinition) []*StructDefinition {
set := make(map[string]map[string]*Property)
descriptions := make(map[string]string)
for i := 0; i < len(definitions); i++ {
definition := definitions[i]
if desc, exists := descriptions[definition.StructName]; exists {
descriptions[definition.StructName] = desc + "\n" + definition.StructDocumentation
}
prev, exists := set[definition.StructName];
if !exists {
set[definition.StructName] = make(map[string]*Property)
for j := 0; j < len(definition.Properties); j++ {
property := definition.Properties[j]
set[definition.StructName][property.Name] = property
}
continue
}
for j := 0; j < len(definition.Properties); j++ {
newProperty := definition.Properties[j]
if oldProperty, exists := prev[newProperty.Name]; exists {
prev[newProperty.Name] = MergeProperty(oldProperty, newProperty)
} else {
prev[newProperty.Name] = newProperty
}
}
}
idx := 0
results := make([]*StructDefinition, len(set))
for structName, propMap := range set {
results[idx] = &StructDefinition{
StructName: structName,
StructDocumentation: descriptions[structName],
Properties: []*Property{},
}
for _, prop := range propMap {
results[idx].Properties = append(results[idx].Properties, set[structName][prop.Name])
}
sort.Slice(results[idx].Properties, func(i, j int) bool {
return results[idx].Properties[i].Name < results[idx].Properties[j].Name
})
idx++
}
// sort them so they generate alphabetized code
sort.Slice(results, func(i, j int) bool {
return results[i].StructName < results[j].StructName
})
return results
}
// Compose merges subsequent resultSets if their struct name and/or property name match. It will
// not override a non-zero value with a zero value.
func Compose(resultSets ...[]*StructDefinition) (results []*StructDefinition) {
structs := make(map[string]*StructDefinition)
for topIndex := 0; topIndex < len(resultSets); topIndex++ {
resultSet := resultSets[topIndex]
for medIndex := 0; medIndex < len(resultSet); medIndex++ {
newDefinition := resultSet[medIndex]
// check if the newDefinition has overriding object-level documentation
if newDefinition.StructDocumentation != "" {
structs[newDefinition.StructName].StructDocumentation = newDefinition.StructDocumentation
}
// merge properties together if a previous struct with the same name was found
if previousDefinition, exists := structs[newDefinition.StructName]; exists {
var finalProperties []*Property
properties := make(map[string]*Property)
// iterate through the properties of the previous definition with this name
for bottomIndex := 0; bottomIndex < len(previousDefinition.Properties); bottomIndex++ {
oldProperty := previousDefinition.Properties[bottomIndex]
properties[oldProperty.Name] = oldProperty
}
// iterate through the properties of the new definition with this name
for bottomIndex := 0; bottomIndex < len(newDefinition.Properties); bottomIndex++ {
newProperty := newDefinition.Properties[bottomIndex]
if oldProperty, exists := properties[newProperty.Name]; exists {
properties[newProperty.Name] = MergeProperty(oldProperty, newProperty)
} else {
properties[newProperty.Name] = newProperty
}
}
// merge them into a new slice
for _, property := range properties {
finalProperties = append(finalProperties, property)
}
// sort them so that they generate alphabetized code
sort.Slice(finalProperties, func(i, j int) bool {
return finalProperties[i].Name < finalProperties[j].Name
})
structs[newDefinition.StructName].Properties = finalProperties
}
structs[newDefinition.StructName] = newDefinition
}
}
// convert map to array
for structName, structValue := range structs {
if structName == "" {
continue
}
results = append(results, structValue)
}
// sort them so they generate alphabetized code
sort.Slice(results, func(i, j int) bool {
return results[i].StructName < results[j].StructName
})
return results
}
func MergeProperties(set map[string]*Property, props ...*Property){
for i := 0; i < len(props); i++ {
if oldProp, exists := set[props[i].Name]; exists {
set[props[i].Name] = MergeProperty(oldProp, props[i])
} else {
set[props[i].Name] = props[i]
}
}
}
// MergeProperty returns a new instance of property overriding properties of oldProperty if they
// do not equal the equivalent property of newProperty
func MergeProperty(oldProperty, newProperty *Property) *Property {
finalProperty := *oldProperty
if newProperty.Documentation != "" && newProperty.Documentation != oldProperty.Documentation {
finalProperty.Documentation = newProperty.Documentation
}
if newProperty.Type != "" && newProperty.Type != oldProperty.Type {
finalProperty.Type = newProperty.Type
}
if len(newProperty.Tags) > 0 && !reflect.DeepEqual(newProperty.Tags, oldProperty.Tags) {
finalProperty.Tags = newProperty.Tags
}
return &finalProperty
}
// Override ...
func Override(structs []*StructDefinition, overrides map[string]*Property) []*StructDefinition {
for i := 0; i < len(structs); i++ {
for j := 0; j < len(structs[i].Properties); j++ {
if property, exists := overrides[structs[i].Properties[j].Name]; exists {
if property.Type != "" {
structs[i].Properties[j].Type = property.Type
}
if property.Documentation != "" {
structs[i].Properties[j].Documentation = property.Documentation
}
if len(property.Tags) != 0 {
structs[i].Properties[j].Tags = property.Tags
}
}
}
}
return structs
}
// NotSubqueryable ...
func NotSubqueryable(properties []*Property) []*Property {
var final []*Property
for i := 0; i < len(properties); i++ {
if strings.Contains(properties[i].Documentation, "subqueries") {
continue
}
if strings.Contains(properties[i].Documentation, "subquery") {
continue
}
if strings.Contains(properties[i].Documentation, "Query this field only if the query result contains no more than one record") {
continue
}
if strings.Contains(properties[i].Documentation, "A relationship lookup") {
continue
}
final = append(final, properties[i])
}
return final
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment