File: dotparser.go

package info (click to toggle)
golang-github-andreykaipov-goobs 0.8.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 1,228 kB
  • sloc: makefile: 32
file content (183 lines) | stat: -rw-r--r-- 4,599 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
package main

import (
	"fmt"
	"os"
	"strings"

	"github.com/dave/jennifer/jen"
)

type keyInfo struct {
	Type      jen.Code
	Comment   string
	NoJSONTag bool
	Embedded  bool
	OmitEmpty bool
}

func parseJenKeysAsMap(lines map[string]keyInfo) (map[string]interface{}, error) {
	// e.g. keeps a reference from a.b to the corresponding struct
	mapReferences := map[string]map[string]interface{}{}

	for _, line := range sortedKeys(lines) {
		typ3 := lines[line].Type

		// prepend $. so the following loop always runs even for parts
		// with no dots, and replace [] as .* to easily treat slices as
		// maps for now
		lineMod := "$." + strings.ReplaceAll(line, "[]", ".*")

		parts := strings.Split(lineMod, ".")

		var m map[string]interface{}
		var ok bool

		for i, part := range parts[:len(parts)-1] {
			key := strings.Join(parts[0:i+1], ".")
			parentKey := strings.Join(parts[0:i], ".")

			if val, ok := mapReferences[parentKey][part].(*jen.Statement); ok {
				// return nil, fmt.Errorf("1: tried to parse '%s' as '%T', but it already terminated at '%#v'", key, m, val)

				fmt.Fprintf(os.Stderr, "! Key %q already terminated at %T, but %q implies it's a map, so that's what we'll do\n", key, val, line)
			}

			if m, ok = mapReferences[key]; !ok {
				m = map[string]interface{}{}
				mapReferences[key] = m
			}

			if parentKey != "" {
				mapReferences[parentKey][part] = m
			}
		}

		lastPart := parts[len(parts)-1]

		if val, ok := m[lastPart]; ok {
			return nil, fmt.Errorf("2: wanted to terminate '%s' at '%#v', but it was already parsed as '%#T'", line, typ3, val)
		}

		m[lastPart] = lines[line]
	}

	final := map[string]interface{}{}
	for k, v := range mapReferences["$"] {
		if !strings.Contains(k, ".") {
			final[k] = v
		}
	}

	return final, nil
}

func parseJenKeysAsStruct(name string, lines map[string]keyInfo) (*jen.Statement, error) {
	m, err := parseJenKeysAsMap(lines)
	if err != nil {
		return nil, err
	}

	// mutually recrusive recursive with traverseStruct
	var traverse func(data interface{}, g *jen.Group, parent string)

	traverseStruct := func(s *jen.Statement, name string, t map[string]interface{}) {
		s.Id(name).StructFunc(func(subg *jen.Group) {
			for _, k := range sortedKeys(t) {
				v := t[k]
				traverse(v, subg, k)
			}
		}).Line()
	}

	// keep track of any anonymous structs as we'll want to make them
	// siblings with the original parent struct
	anonymousStructs := []*jen.Statement{}

	traverse = func(data interface{}, g *jen.Group, parent string) {
		var idType jen.Code
		id := pascal(parent)
		tag := strings.TrimSuffix(parent, "[]")

		switch t := data.(type) {
		case map[string]interface{}:
			// if there's an * in the keys, the parent key we're on
			// must have been a slice, so use "[]" as the marker,
			// and redo the recursion
			for _, k := range sortedKeys(t) {
				v := t[k]
				if k == "*" {
					traverse(v, g, parent+"[]")
					return
				}
			}

			// Since the type to our nested struct will always be
			// a pointer, it'll be omitted during encoding if that
			// value is ever nil, which seems always like the
			// preferred behavior. 🤷
			tag += ",omitempty"

			idType = jen.Op("*").Id(id) // is a nested anon struct, so use a pointer to it itself as the type
			idDeplural := id

			if strings.HasSuffix(id, "[]") {
				id = strings.TrimSuffix(id, "[]")
				idDeplural = id

				// try to depluralize lol
				// TODO add more exhaustive rules as necessary
				// because this isn't really robust
				if strings.HasSuffix(id, "s") {
					fmt.Printf("  ! %s is a slice and looks plural, we'll try to depluralize...\n", id)
					idDeplural = strings.TrimSuffix(id, "s")
				}

				idType = jen.Index().Op("*").Id(idDeplural)
			}

			s := jen.Empty()
			traverseStruct(s, idDeplural, t)
			anonymousStructs = append(anonymousStructs, s)
		case keyInfo:
			idType = t.Type

			if t.Comment != "" {
				g.Comment(t.Comment)
			}
			if t.Embedded {
				id = ""
				t.NoJSONTag = true
			}
			if t.OmitEmpty {
				tag += ",omitempty"
			}
			if t.NoJSONTag {
				tag = ""
			}
		default:
			panic("unhandled case idk")
		}

		g.Id(id).Add(idType).Do(func(s *jen.Statement) {
			if tag == "" {
				return
			}
			s.Tag(map[string]string{"json": tag})
		}).Line()
	}

	s := jen.Type()
	traverseStruct(s, name, m)
	for _, q := range anonymousStructs {
		s.Line().Type().Add(q).Line()
	}
	return s, nil
}

func pascal(text string) string {
	nodash := strings.ReplaceAll(text, "-", " ")
	noundies := strings.ReplaceAll(nodash, "_", " ")
	titled := strings.Title(noundies)
	return strings.ReplaceAll(titled, " ", "")
}