Skip to content

Commit 30e9bf8

Browse files
karimkhaleelstefanhaller
authored andcommitted
Use refs in jsonschema userconfig generator
This makes it possible to use recursive structures in the user config.
1 parent 62c6ba7 commit 30e9bf8

File tree

11 files changed

+1570
-1823
lines changed

11 files changed

+1570
-1823
lines changed

docs/Config.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,11 @@ os:
411411
# window is closed.
412412
editAtLineAndWait: ""
413413

414+
# Whether lazygit suspends until an edit process returns
415+
# Pointer to bool so that we can distinguish unset (nil) from false.
416+
# We're naming this `editInTerminal` for backwards compatibility
417+
editInTerminal: false
418+
414419
# For opening a directory in an editor
415420
openDirInEditor: ""
416421

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ require (
1111
github.com/gdamore/tcell/v2 v2.8.1
1212
github.com/go-errors/errors v1.5.1
1313
github.com/gookit/color v1.4.2
14-
github.com/iancoleman/orderedmap v0.3.0
1514
github.com/imdario/mergo v0.3.11
1615
github.com/integrii/flaggy v1.4.0
1716
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,6 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
171171
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
172172
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
173173
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
174-
github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc=
175-
github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE=
176174
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
177175
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
178176
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=

pkg/jsonschema/generate.go

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,41 +7,76 @@ import (
77
"fmt"
88
"os"
99
"reflect"
10+
"strings"
1011

1112
"github.com/jesseduffield/lazycore/pkg/utils"
1213
"github.com/jesseduffield/lazygit/pkg/config"
1314
"github.com/karimkhaleel/jsonschema"
15+
"github.com/samber/lo"
1416
)
1517

1618
func GetSchemaDir() string {
1719
return utils.GetLazyRootDirectory() + "/schema"
1820
}
1921

20-
func GenerateSchema() {
22+
func GenerateSchema() *jsonschema.Schema {
2123
schema := customReflect(&config.UserConfig{})
2224
obj, _ := json.MarshalIndent(schema, "", " ")
2325
obj = append(obj, '\n')
2426

2527
if err := os.WriteFile(GetSchemaDir()+"/config.json", obj, 0o644); err != nil {
2628
fmt.Println("Error writing to file:", err)
27-
return
29+
return nil
30+
}
31+
return schema
32+
}
33+
34+
func getSubSchema(rootSchema, parentSchema *jsonschema.Schema, key string) *jsonschema.Schema {
35+
subSchema, found := parentSchema.Properties.Get(key)
36+
if !found {
37+
panic(fmt.Sprintf("Failed to find subSchema at %s on parent", key))
38+
}
39+
40+
// This means the schema is defined on the rootSchema's Definitions
41+
if subSchema.Ref != "" {
42+
key, _ = strings.CutPrefix(subSchema.Ref, "#/$defs/")
43+
refSchema, ok := rootSchema.Definitions[key]
44+
if !ok {
45+
panic(fmt.Sprintf("Failed to find #/$defs/%s", key))
46+
}
47+
refSchema.Description = subSchema.Description
48+
return refSchema
2849
}
50+
51+
return subSchema
2952
}
3053

3154
func customReflect(v *config.UserConfig) *jsonschema.Schema {
32-
defaultConfig := config.GetDefaultConfig()
33-
r := &jsonschema.Reflector{FieldNameTag: "yaml", RequiredFromJSONSchemaTags: true, DoNotReference: true}
55+
r := &jsonschema.Reflector{FieldNameTag: "yaml", RequiredFromJSONSchemaTags: true}
3456
if err := r.AddGoComments("github.com/jesseduffield/lazygit/pkg/config", "../config"); err != nil {
3557
panic(err)
3658
}
3759
schema := r.Reflect(v)
60+
defaultConfig := config.GetDefaultConfig()
61+
userConfigSchema := schema.Definitions["UserConfig"]
62+
63+
defaultValue := reflect.ValueOf(defaultConfig).Elem()
64+
65+
yamlToFieldNames := lo.Invert(userConfigSchema.OriginalPropertiesMapping)
3866

39-
setDefaultVals(defaultConfig, schema)
67+
for pair := userConfigSchema.Properties.Oldest(); pair != nil; pair = pair.Next() {
68+
yamlName := pair.Key
69+
fieldName := yamlToFieldNames[yamlName]
70+
71+
subSchema := getSubSchema(schema, userConfigSchema, yamlName)
72+
73+
setDefaultVals(schema, subSchema, defaultValue.FieldByName(fieldName).Interface())
74+
}
4075

4176
return schema
4277
}
4378

44-
func setDefaultVals(defaults any, schema *jsonschema.Schema) {
79+
func setDefaultVals(rootSchema, schema *jsonschema.Schema, defaults any) {
4580
t := reflect.TypeOf(defaults)
4681
v := reflect.ValueOf(defaults)
4782

@@ -50,6 +85,24 @@ func setDefaultVals(defaults any, schema *jsonschema.Schema) {
5085
v = v.Elem()
5186
}
5287

88+
k := t.Kind()
89+
_ = k
90+
91+
switch t.Kind() {
92+
case reflect.Bool:
93+
schema.Default = v.Bool()
94+
case reflect.Int:
95+
schema.Default = v.Int()
96+
case reflect.String:
97+
schema.Default = v.String()
98+
default:
99+
// Do nothing
100+
}
101+
102+
if t.Kind() != reflect.Struct {
103+
return
104+
}
105+
53106
for i := 0; i < t.NumField(); i++ {
54107
value := v.Field(i).Interface()
55108
parentKey := t.Field(i).Name
@@ -59,13 +112,10 @@ func setDefaultVals(defaults any, schema *jsonschema.Schema) {
59112
continue
60113
}
61114

62-
subSchema, ok := schema.Properties.Get(key)
63-
if !ok {
64-
continue
65-
}
115+
subSchema := getSubSchema(rootSchema, schema, key)
66116

67117
if isStruct(value) {
68-
setDefaultVals(value, subSchema)
118+
setDefaultVals(rootSchema, subSchema, value)
69119
} else if !isZeroValue(value) {
70120
subSchema.Default = value
71121
}

pkg/jsonschema/generate_config_docs.go

Lines changed: 59 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@ package jsonschema
22

33
import (
44
"bytes"
5-
"encoding/json"
65
"errors"
76
"fmt"
87
"os"
98
"strings"
109

11-
"github.com/iancoleman/orderedmap"
1210
"github.com/jesseduffield/lazycore/pkg/utils"
11+
"github.com/karimkhaleel/jsonschema"
1312
"github.com/samber/lo"
1413

1514
"gopkg.in/yaml.v3"
@@ -106,16 +105,7 @@ func (n *Node) MarshalYAML() (interface{}, error) {
106105
setComment(&keyNode, n.Description)
107106
}
108107

109-
if n.Default != nil {
110-
valueNode := yaml.Node{
111-
Kind: yaml.ScalarNode,
112-
}
113-
err := valueNode.Encode(n.Default)
114-
if err != nil {
115-
return nil, err
116-
}
117-
node.Content = append(node.Content, &keyNode, &valueNode)
118-
} else if len(n.Children) > 0 {
108+
if len(n.Children) > 0 {
119109
childrenNode := yaml.Node{
120110
Kind: yaml.MappingNode,
121111
}
@@ -136,60 +126,18 @@ func (n *Node) MarshalYAML() (interface{}, error) {
136126
childrenNode.Content = append(childrenNode.Content, childYaml.(*yaml.Node).Content...)
137127
}
138128
node.Content = append(node.Content, &keyNode, &childrenNode)
139-
}
140-
141-
return &node, nil
142-
}
143-
144-
func getDescription(v *orderedmap.OrderedMap) string {
145-
description, ok := v.Get("description")
146-
if !ok {
147-
description = ""
148-
}
149-
return description.(string)
150-
}
151-
152-
func getDefault(v *orderedmap.OrderedMap) (error, any) {
153-
defaultValue, ok := v.Get("default")
154-
if ok {
155-
return nil, defaultValue
156-
}
157-
158-
dataType, ok := v.Get("type")
159-
if ok {
160-
dataTypeString := dataType.(string)
161-
if dataTypeString == "string" {
162-
return nil, ""
129+
} else {
130+
valueNode := yaml.Node{
131+
Kind: yaml.ScalarNode,
163132
}
133+
err := valueNode.Encode(n.Default)
134+
if err != nil {
135+
return nil, err
136+
}
137+
node.Content = append(node.Content, &keyNode, &valueNode)
164138
}
165139

166-
return errors.New("Failed to get default value"), nil
167-
}
168-
169-
func parseNode(parent *Node, name string, value *orderedmap.OrderedMap) {
170-
description := getDescription(value)
171-
err, defaultValue := getDefault(value)
172-
if err == nil {
173-
leaf := &Node{Name: name, Description: description, Default: defaultValue}
174-
parent.Children = append(parent.Children, leaf)
175-
}
176-
177-
properties, ok := value.Get("properties")
178-
if !ok {
179-
return
180-
}
181-
182-
orderedProperties := properties.(orderedmap.OrderedMap)
183-
184-
node := &Node{Name: name, Description: description}
185-
parent.Children = append(parent.Children, node)
186-
187-
keys := orderedProperties.Keys()
188-
for _, name := range keys {
189-
value, _ := orderedProperties.Get(name)
190-
typedValue := value.(orderedmap.OrderedMap)
191-
parseNode(node, name, &typedValue)
192-
}
140+
return &node, nil
193141
}
194142

195143
func writeToConfigDocs(config []byte) error {
@@ -222,31 +170,12 @@ func writeToConfigDocs(config []byte) error {
222170
return nil
223171
}
224172

225-
func GenerateConfigDocs() {
226-
content, err := os.ReadFile(GetSchemaDir() + "/config.json")
227-
if err != nil {
228-
panic("Error reading config.json")
173+
func GenerateConfigDocs(schema *jsonschema.Schema) {
174+
rootNode := &Node{
175+
Children: make([]*Node, 0),
229176
}
230177

231-
schema := orderedmap.New()
232-
233-
err = json.Unmarshal(content, &schema)
234-
if err != nil {
235-
panic("Failed to unmarshal config.json")
236-
}
237-
238-
root, ok := schema.Get("properties")
239-
if !ok {
240-
panic("properties key not found in schema")
241-
}
242-
orderedRoot := root.(orderedmap.OrderedMap)
243-
244-
rootNode := Node{}
245-
for _, name := range orderedRoot.Keys() {
246-
value, _ := orderedRoot.Get(name)
247-
typedValue := value.(orderedmap.OrderedMap)
248-
parseNode(&rootNode, name, &typedValue)
249-
}
178+
recurseOverSchema(schema, schema.Definitions["UserConfig"], rootNode)
250179

251180
var buffer bytes.Buffer
252181
encoder := yaml.NewEncoder(&buffer)
@@ -262,8 +191,51 @@ func GenerateConfigDocs() {
262191

263192
config := prepareMarshalledConfig(buffer)
264193

265-
err = writeToConfigDocs(config)
194+
err := writeToConfigDocs(config)
266195
if err != nil {
267196
panic(err)
268197
}
269198
}
199+
200+
func recurseOverSchema(rootSchema, schema *jsonschema.Schema, parent *Node) {
201+
if schema == nil || schema.Properties == nil || schema.Properties.Len() == 0 {
202+
return
203+
}
204+
205+
for pair := schema.Properties.Oldest(); pair != nil; pair = pair.Next() {
206+
subSchema := getSubSchema(rootSchema, schema, pair.Key)
207+
208+
// Skip empty objects
209+
if subSchema.Type == "object" && subSchema.Properties == nil {
210+
continue
211+
}
212+
213+
// Skip empty arrays
214+
if isZeroValue(subSchema.Default) && subSchema.Type == "array" {
215+
continue
216+
}
217+
218+
node := Node{
219+
Name: pair.Key,
220+
Description: subSchema.Description,
221+
Default: getZeroValue(subSchema.Default, subSchema.Type),
222+
}
223+
parent.Children = append(parent.Children, &node)
224+
recurseOverSchema(rootSchema, subSchema, &node)
225+
}
226+
}
227+
228+
func getZeroValue(val any, t string) any {
229+
if !isZeroValue(val) {
230+
return val
231+
}
232+
233+
switch t {
234+
case "string":
235+
return ""
236+
case "boolean":
237+
return false
238+
default:
239+
return nil
240+
}
241+
}

pkg/jsonschema/generator.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ import (
1010

1111
func main() {
1212
fmt.Printf("Generating jsonschema in %s...\n", jsonschema.GetSchemaDir())
13-
jsonschema.GenerateSchema()
14-
jsonschema.GenerateConfigDocs()
13+
schema := jsonschema.GenerateSchema()
14+
jsonschema.GenerateConfigDocs(schema)
1515
}

0 commit comments

Comments
 (0)