Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use refs in jsonschema #4309

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/Config.md
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,9 @@ os:
# window is closed.
editAtLineAndWait: ""

# Whether lazygit suspends until an edit process returns
editInTerminal: false

# For opening a directory in an editor
openDirInEditor: ""

Expand Down
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ require (
github.com/gdamore/tcell/v2 v2.8.1
github.com/go-errors/errors v1.5.1
github.com/gookit/color v1.4.2
github.com/iancoleman/orderedmap v0.3.0
github.com/imdario/mergo v0.3.11
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,6 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc=
github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
Expand Down
4 changes: 2 additions & 2 deletions pkg/config/user_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -557,8 +557,8 @@ type OSConfig struct {
EditAtLineAndWait string `yaml:"editAtLineAndWait,omitempty"`

// Whether lazygit suspends until an edit process returns
// Pointer to bool so that we can distinguish unset (nil) from false.
// We're naming this `editInTerminal` for backwards compatibility
// [dev] Pointer to bool so that we can distinguish unset (nil) from false.
// [dev] We're naming this `editInTerminal` for backwards compatibility
SuspendOnEdit *bool `yaml:"editInTerminal,omitempty"`

// For opening a directory in an editor
Expand Down
72 changes: 61 additions & 11 deletions pkg/jsonschema/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,76 @@ import (
"fmt"
"os"
"reflect"
"strings"

"github.com/jesseduffield/lazycore/pkg/utils"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/karimkhaleel/jsonschema"
"github.com/samber/lo"
)

func GetSchemaDir() string {
return utils.GetLazyRootDirectory() + "/schema"
}

func GenerateSchema() {
func GenerateSchema() *jsonschema.Schema {
schema := customReflect(&config.UserConfig{})
obj, _ := json.MarshalIndent(schema, "", " ")
obj = append(obj, '\n')

if err := os.WriteFile(GetSchemaDir()+"/config.json", obj, 0o644); err != nil {
fmt.Println("Error writing to file:", err)
return
return nil
}
return schema
}

func getSubSchema(rootSchema, parentSchema *jsonschema.Schema, key string) *jsonschema.Schema {
subSchema, found := parentSchema.Properties.Get(key)
if !found {
panic(fmt.Sprintf("Failed to find subSchema at %s on parent", key))
}

// This means the schema is defined on the rootSchema's Definitions
if subSchema.Ref != "" {
key, _ = strings.CutPrefix(subSchema.Ref, "#/$defs/")
refSchema, ok := rootSchema.Definitions[key]
if !ok {
panic(fmt.Sprintf("Failed to find #/$defs/%s", key))
}
refSchema.Description = subSchema.Description
return refSchema
}

return subSchema
}

func customReflect(v *config.UserConfig) *jsonschema.Schema {
defaultConfig := config.GetDefaultConfig()
r := &jsonschema.Reflector{FieldNameTag: "yaml", RequiredFromJSONSchemaTags: true, DoNotReference: true}
r := &jsonschema.Reflector{FieldNameTag: "yaml", RequiredFromJSONSchemaTags: true}
if err := r.AddGoComments("github.com/jesseduffield/lazygit/pkg/config", "../config"); err != nil {
panic(err)
}
schema := r.Reflect(v)
defaultConfig := config.GetDefaultConfig()
userConfigSchema := schema.Definitions["UserConfig"]

defaultValue := reflect.ValueOf(defaultConfig).Elem()

yamlToFieldNames := lo.Invert(userConfigSchema.OriginalPropertiesMapping)

setDefaultVals(defaultConfig, schema)
for pair := userConfigSchema.Properties.Oldest(); pair != nil; pair = pair.Next() {
yamlName := pair.Key
fieldName := yamlToFieldNames[yamlName]

subSchema := getSubSchema(schema, userConfigSchema, yamlName)

setDefaultVals(schema, subSchema, defaultValue.FieldByName(fieldName).Interface())
}

return schema
}

func setDefaultVals(defaults any, schema *jsonschema.Schema) {
func setDefaultVals(rootSchema, schema *jsonschema.Schema, defaults any) {
t := reflect.TypeOf(defaults)
v := reflect.ValueOf(defaults)

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

k := t.Kind()
_ = k

switch t.Kind() {
case reflect.Bool:
schema.Default = v.Bool()
case reflect.Int:
schema.Default = v.Int()
case reflect.String:
schema.Default = v.String()
default:
// Do nothing
}

if t.Kind() != reflect.Struct {
return
}

for i := 0; i < t.NumField(); i++ {
value := v.Field(i).Interface()
parentKey := t.Field(i).Name
Expand All @@ -59,13 +112,10 @@ func setDefaultVals(defaults any, schema *jsonschema.Schema) {
continue
}

subSchema, ok := schema.Properties.Get(key)
if !ok {
continue
}
subSchema := getSubSchema(rootSchema, schema, key)

if isStruct(value) {
setDefaultVals(value, subSchema)
setDefaultVals(rootSchema, subSchema, value)
} else if !isZeroValue(value) {
subSchema.Default = value
}
Expand Down
155 changes: 67 additions & 88 deletions pkg/jsonschema/generate_config_docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@ package jsonschema

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"strings"

"github.com/iancoleman/orderedmap"
"github.com/jesseduffield/lazycore/pkg/utils"
"github.com/karimkhaleel/jsonschema"
"github.com/samber/lo"

"gopkg.in/yaml.v3"
Expand Down Expand Up @@ -78,13 +77,20 @@ func prepareMarshalledConfig(buffer bytes.Buffer) []byte {
}

func setComment(yamlNode *yaml.Node, description string) {
// Filter out lines containing "[dev]"; this allows us to add developer
// documentation to properties that don't get included in the docs
lines := strings.Split(description, "\n")
lines = lo.Filter(lines, func(s string, _ int) bool {
return !strings.Contains(s, "[dev]")
})

// Workaround for the way yaml formats the HeadComment if it contains
// blank lines: it renders these without a leading "#", but we want a
// leading "#" even on blank lines. However, yaml respects it if the
// HeadComment already contains a leading "#", so we prefix all lines
// (including blank ones) with "#".
yamlNode.HeadComment = strings.Join(
lo.Map(strings.Split(description, "\n"), func(s string, _ int) string {
lo.Map(lines, func(s string, _ int) string {
if s == "" {
return "#" // avoid trailing space on blank lines
}
Expand All @@ -106,16 +112,7 @@ func (n *Node) MarshalYAML() (interface{}, error) {
setComment(&keyNode, n.Description)
}

if n.Default != nil {
valueNode := yaml.Node{
Kind: yaml.ScalarNode,
}
err := valueNode.Encode(n.Default)
if err != nil {
return nil, err
}
node.Content = append(node.Content, &keyNode, &valueNode)
} else if len(n.Children) > 0 {
if len(n.Children) > 0 {
childrenNode := yaml.Node{
Kind: yaml.MappingNode,
}
Expand All @@ -136,60 +133,18 @@ func (n *Node) MarshalYAML() (interface{}, error) {
childrenNode.Content = append(childrenNode.Content, childYaml.(*yaml.Node).Content...)
}
node.Content = append(node.Content, &keyNode, &childrenNode)
}

return &node, nil
}

func getDescription(v *orderedmap.OrderedMap) string {
description, ok := v.Get("description")
if !ok {
description = ""
}
return description.(string)
}

func getDefault(v *orderedmap.OrderedMap) (error, any) {
defaultValue, ok := v.Get("default")
if ok {
return nil, defaultValue
}

dataType, ok := v.Get("type")
if ok {
dataTypeString := dataType.(string)
if dataTypeString == "string" {
return nil, ""
} else {
valueNode := yaml.Node{
Kind: yaml.ScalarNode,
}
err := valueNode.Encode(n.Default)
if err != nil {
return nil, err
}
node.Content = append(node.Content, &keyNode, &valueNode)
}

return errors.New("Failed to get default value"), nil
}

func parseNode(parent *Node, name string, value *orderedmap.OrderedMap) {
description := getDescription(value)
err, defaultValue := getDefault(value)
if err == nil {
leaf := &Node{Name: name, Description: description, Default: defaultValue}
parent.Children = append(parent.Children, leaf)
}

properties, ok := value.Get("properties")
if !ok {
return
}

orderedProperties := properties.(orderedmap.OrderedMap)

node := &Node{Name: name, Description: description}
parent.Children = append(parent.Children, node)

keys := orderedProperties.Keys()
for _, name := range keys {
value, _ := orderedProperties.Get(name)
typedValue := value.(orderedmap.OrderedMap)
parseNode(node, name, &typedValue)
}
return &node, nil
}

func writeToConfigDocs(config []byte) error {
Expand Down Expand Up @@ -222,31 +177,12 @@ func writeToConfigDocs(config []byte) error {
return nil
}

func GenerateConfigDocs() {
content, err := os.ReadFile(GetSchemaDir() + "/config.json")
if err != nil {
panic("Error reading config.json")
}

schema := orderedmap.New()

err = json.Unmarshal(content, &schema)
if err != nil {
panic("Failed to unmarshal config.json")
}

root, ok := schema.Get("properties")
if !ok {
panic("properties key not found in schema")
func GenerateConfigDocs(schema *jsonschema.Schema) {
rootNode := &Node{
Children: make([]*Node, 0),
}
orderedRoot := root.(orderedmap.OrderedMap)

rootNode := Node{}
for _, name := range orderedRoot.Keys() {
value, _ := orderedRoot.Get(name)
typedValue := value.(orderedmap.OrderedMap)
parseNode(&rootNode, name, &typedValue)
}
recurseOverSchema(schema, schema.Definitions["UserConfig"], rootNode)

var buffer bytes.Buffer
encoder := yaml.NewEncoder(&buffer)
Expand All @@ -262,8 +198,51 @@ func GenerateConfigDocs() {

config := prepareMarshalledConfig(buffer)

err = writeToConfigDocs(config)
err := writeToConfigDocs(config)
if err != nil {
panic(err)
}
}

func recurseOverSchema(rootSchema, schema *jsonschema.Schema, parent *Node) {
if schema == nil || schema.Properties == nil || schema.Properties.Len() == 0 {
return
}

for pair := schema.Properties.Oldest(); pair != nil; pair = pair.Next() {
subSchema := getSubSchema(rootSchema, schema, pair.Key)

// Skip empty objects
if subSchema.Type == "object" && subSchema.Properties == nil {
continue
}

// Skip empty arrays
if isZeroValue(subSchema.Default) && subSchema.Type == "array" {
continue
}

node := Node{
Name: pair.Key,
Description: subSchema.Description,
Default: getZeroValue(subSchema.Default, subSchema.Type),
}
parent.Children = append(parent.Children, &node)
recurseOverSchema(rootSchema, subSchema, &node)
}
}

func getZeroValue(val any, t string) any {
if !isZeroValue(val) {
return val
}

switch t {
case "string":
return ""
case "boolean":
return false
default:
return nil
}
}
4 changes: 2 additions & 2 deletions pkg/jsonschema/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ import (

func main() {
fmt.Printf("Generating jsonschema in %s...\n", jsonschema.GetSchemaDir())
jsonschema.GenerateSchema()
jsonschema.GenerateConfigDocs()
schema := jsonschema.GenerateSchema()
jsonschema.GenerateConfigDocs(schema)
}
Loading
Loading