Created
January 1, 2021 17:31
-
-
Save beeekind/4c25283856f774040ceef1df4e992c27 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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