From ae7de0d87ce8aaf0c2fe92507b1a215015e2d17c Mon Sep 17 00:00:00 2001 From: Karim Khaleel Date: Sun, 23 Mar 2025 00:55:13 -0400 Subject: [PATCH 1/2] Use keyrenamer function in jsonschema config generation --- go.mod | 2 +- go.sum | 4 +- pkg/config/user_config.go | 4 +- .../controllers/local_commits_controller.go | 2 +- pkg/integration/tests/commit/create_tag.go | 2 +- .../tests/tag/force_tag_annotated.go | 2 +- .../tests/tag/force_tag_lightweight.go | 2 +- pkg/jsonschema/generate.go | 20 +- .../karimkhaleel/jsonschema/.golangci.yml | 21 +- .../karimkhaleel/jsonschema/README.md | 16 +- .../jsonschema/comment_extractor.go | 93 ------- .../karimkhaleel/jsonschema/reflect.go | 243 ++++++++---------- .../jsonschema/reflect_comments.go | 146 +++++++++++ .../karimkhaleel/jsonschema/schema.go | 94 +++++++ vendor/modules.txt | 2 +- 15 files changed, 385 insertions(+), 268 deletions(-) delete mode 100644 vendor/github.com/karimkhaleel/jsonschema/comment_extractor.go create mode 100644 vendor/github.com/karimkhaleel/jsonschema/reflect_comments.go create mode 100644 vendor/github.com/karimkhaleel/jsonschema/schema.go diff --git a/go.mod b/go.mod index 1cc02b1d7b1..9f3e1adce8a 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 - github.com/karimkhaleel/jsonschema v0.0.0-20231001195015-d933f0d94ea3 + github.com/karimkhaleel/jsonschema v0.0.0-20250323054317-7eb70f14797b github.com/kyokomi/emoji/v2 v2.2.8 github.com/lucasb-eyer/go-colorful v1.2.0 github.com/mattn/go-runewidth v0.0.16 diff --git a/go.sum b/go.sum index 3750f2e9f52..2b625b7d60f 100644 --- a/go.sum +++ b/go.sum @@ -200,8 +200,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= -github.com/karimkhaleel/jsonschema v0.0.0-20231001195015-d933f0d94ea3 h1:s995u+gNQADMaixtNOs+jilRC/Q78q0UXSI7+4T0cDE= -github.com/karimkhaleel/jsonschema v0.0.0-20231001195015-d933f0d94ea3/go.mod h1:MCbEh21gjOzxc31udr3u4QM9DAdf8TFJCZz3u5hYIxA= +github.com/karimkhaleel/jsonschema v0.0.0-20250323054317-7eb70f14797b h1:fO3alvlIx8s0Ou+lkV6I46UzkUALHhdQYEWWqYMsirY= +github.com/karimkhaleel/jsonschema v0.0.0-20250323054317-7eb70f14797b/go.mod h1:MCbEh21gjOzxc31udr3u4QM9DAdf8TFJCZz3u5hYIxA= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 62070123563..c7efeea652c 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -504,7 +504,7 @@ type KeybindingCommitsConfig struct { CherryPickCopy string `yaml:"cherryPickCopy"` PasteCommits string `yaml:"pasteCommits"` MarkCommitAsBaseForRebase string `yaml:"markCommitAsBaseForRebase"` - CreateTag string `yaml:"tagCommit"` + TagCommit string `yaml:"tagCommit"` CheckoutCommit string `yaml:"checkoutCommit"` ResetCherryPick string `yaml:"resetCherryPick"` CopyCommitAttributeToClipboard string `yaml:"copyCommitAttributeToClipboard"` @@ -963,7 +963,7 @@ func GetDefaultConfig() *UserConfig { CherryPickCopy: "C", PasteCommits: "V", MarkCommitAsBaseForRebase: "B", - CreateTag: "T", + TagCommit: "T", CheckoutCommit: "", ResetCherryPick: "", CopyCommitAttributeToClipboard: "y", diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index 53885c8f5ff..91c2f507843 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -255,7 +255,7 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ Tooltip: self.c.Tr.RevertCommitTooltip, }, { - Key: opts.GetKey(opts.Config.Commits.CreateTag), + Key: opts.GetKey(opts.Config.Commits.TagCommit), Handler: self.withItem(self.createTag), GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.TagCommit, diff --git a/pkg/integration/tests/commit/create_tag.go b/pkg/integration/tests/commit/create_tag.go index f068f271bbe..6deb292d94c 100644 --- a/pkg/integration/tests/commit/create_tag.go +++ b/pkg/integration/tests/commit/create_tag.go @@ -21,7 +21,7 @@ var CreateTag = NewIntegrationTest(NewIntegrationTestArgs{ Contains("two").IsSelected(), Contains("one"), ). - Press(keys.Commits.CreateTag) + Press(keys.Commits.TagCommit) t.ExpectPopup().CommitMessagePanel(). Title(Equals("Tag name")). diff --git a/pkg/integration/tests/tag/force_tag_annotated.go b/pkg/integration/tests/tag/force_tag_annotated.go index 1f9e2f09b8a..90c27cc1118 100644 --- a/pkg/integration/tests/tag/force_tag_annotated.go +++ b/pkg/integration/tests/tag/force_tag_annotated.go @@ -22,7 +22,7 @@ var ForceTagAnnotated = NewIntegrationTest(NewIntegrationTestArgs{ Contains("second commit").IsSelected(), Contains("new-tag").Contains("first commit"), ). - Press(keys.Commits.CreateTag). + Press(keys.Commits.TagCommit). Tap(func() { t.ExpectPopup().CommitMessagePanel(). Title(Equals("Tag name")). diff --git a/pkg/integration/tests/tag/force_tag_lightweight.go b/pkg/integration/tests/tag/force_tag_lightweight.go index 1f24b624c3c..6f5a31bd1d8 100644 --- a/pkg/integration/tests/tag/force_tag_lightweight.go +++ b/pkg/integration/tests/tag/force_tag_lightweight.go @@ -22,7 +22,7 @@ var ForceTagLightweight = NewIntegrationTest(NewIntegrationTestArgs{ Contains("second commit").IsSelected(), Contains("new-tag").Contains("first commit"), ). - Press(keys.Commits.CreateTag). + Press(keys.Commits.TagCommit). Tap(func() { t.ExpectPopup().CommitMessagePanel(). Title(Equals("Tag name")). diff --git a/pkg/jsonschema/generate.go b/pkg/jsonschema/generate.go index d8514e3b677..ddb23020751 100644 --- a/pkg/jsonschema/generate.go +++ b/pkg/jsonschema/generate.go @@ -52,7 +52,14 @@ func getSubSchema(rootSchema, parentSchema *jsonschema.Schema, key string) *json } func customReflect(v *config.UserConfig) *jsonschema.Schema { - r := &jsonschema.Reflector{FieldNameTag: "yaml", RequiredFromJSONSchemaTags: true} + yamlToFieldNames := make(map[string]string) + keyNamer := func(yamlName string, originalFieldName string) string { + yamlToFieldNames[yamlName] = originalFieldName + yamlToFieldNames[originalFieldName] = yamlName + return yamlName + } + + r := &jsonschema.Reflector{FieldNameTag: "yaml", RequiredFromJSONSchemaTags: true, KeyNamerWithOriginalFieldName: keyNamer} if err := r.AddGoComments("github.com/jesseduffield/lazygit/pkg/config", "../config"); err != nil { panic(err) } @@ -63,15 +70,13 @@ func customReflect(v *config.UserConfig) *jsonschema.Schema { defaultValue := reflect.ValueOf(defaultConfig).Elem() - yamlToFieldNames := lo.Invert(userConfigSchema.OriginalPropertiesMapping) - 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()) + setDefaultVals(schema, subSchema, defaultValue.FieldByName(fieldName).Interface(), yamlToFieldNames) } return schema @@ -87,7 +92,7 @@ func filterOutDevComments(r *jsonschema.Reflector) { } } -func setDefaultVals(rootSchema, schema *jsonschema.Schema, defaults any) { +func setDefaultVals(rootSchema, schema *jsonschema.Schema, defaults any, yamlToFieldNames map[string]string) { t := reflect.TypeOf(defaults) v := reflect.ValueOf(defaults) @@ -118,15 +123,16 @@ func setDefaultVals(rootSchema, schema *jsonschema.Schema, defaults any) { value := v.Field(i).Interface() parentKey := t.Field(i).Name - key, ok := schema.OriginalPropertiesMapping[parentKey] + key, ok := yamlToFieldNames[parentKey] if !ok { + fmt.Println(key) continue } subSchema := getSubSchema(rootSchema, schema, key) if isStruct(value) { - setDefaultVals(rootSchema, subSchema, value) + setDefaultVals(rootSchema, subSchema, value, yamlToFieldNames) } else if !isZeroValue(value) { subSchema.Default = value } diff --git a/vendor/github.com/karimkhaleel/jsonschema/.golangci.yml b/vendor/github.com/karimkhaleel/jsonschema/.golangci.yml index 3dac8a37df3..b89b2e124d3 100644 --- a/vendor/github.com/karimkhaleel/jsonschema/.golangci.yml +++ b/vendor/github.com/karimkhaleel/jsonschema/.golangci.yml @@ -1,11 +1,6 @@ run: tests: true max-same-issues: 50 - skip-dirs: - - resources - - old - skip-files: - - cmd/protopkg/main.go output: print-issued-lines: false @@ -19,13 +14,12 @@ linters: - unconvert - goimports - unused - - vetshadow + - govet - nakedret - errcheck - revive - ineffassign - goconst - - vet - unparam - gofmt @@ -45,14 +39,19 @@ linters-settings: - ifElseChain gofmt: rewrite-rules: - - pattern: 'interface{}' - replacement: 'any' - - pattern: 'a[b:len(a)]' - replacement: 'a[b:]' + - pattern: "interface{}" + replacement: "any" + - pattern: "a[b:len(a)]" + replacement: "a[b:]" issues: max-per-linter: 0 max-same: 0 + exclude-dirs: + - resources + - old + exclude-files: + - cmd/protopkg/main.go exclude-use-default: false exclude: # Captured by errcheck. diff --git a/vendor/github.com/karimkhaleel/jsonschema/README.md b/vendor/github.com/karimkhaleel/jsonschema/README.md index 65ed8aea2af..27b362e1dd7 100644 --- a/vendor/github.com/karimkhaleel/jsonschema/README.md +++ b/vendor/github.com/karimkhaleel/jsonschema/README.md @@ -4,6 +4,7 @@ [![Test Go](https://github.com/invopop/jsonschema/actions/workflows/test.yaml/badge.svg)](https://github.com/invopop/jsonschema/actions/workflows/test.yaml) [![Go Report Card](https://goreportcard.com/badge/github.com/invopop/jsonschema)](https://goreportcard.com/report/github.com/invopop/jsonschema) [![GoDoc](https://godoc.org/github.com/invopop/jsonschema?status.svg)](https://godoc.org/github.com/invopop/jsonschema) +[![codecov](https://codecov.io/gh/invopop/jsonschema/graph/badge.svg?token=JMEB8W8GNZ)](https://codecov.io/gh/invopop/jsonschema) ![Latest Tag](https://img.shields.io/github/v/tag/invopop/jsonschema) This package can be used to generate [JSON Schemas](http://json-schema.org/latest/json-schema-validation.html) from Go types through reflection. @@ -52,10 +53,10 @@ jsonschema.Reflect(&TestUser{}) ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/invopop/jsonschema_test/sample-user", - "$ref": "#/$defs/SampleUser", + "$id": "https://github.com/invopop/jsonschema_test/test-user", + "$ref": "#/$defs/TestUser", "$defs": { - "SampleUser": { + "TestUser": { "oneOf": [ { "required": ["birth_date"], @@ -304,9 +305,14 @@ As you can see, if a field name has a `json:""` tag set, the `key` argument to ` Sometimes it can be useful to have custom JSON Marshal and Unmarshal methods in your structs that automatically convert for example a string into an object. -To override auto-generating an object type for your type, implement the `JSONSchema() *Schema` method and whatever is defined will be provided in the schema definitions. +This library will recognize and attempt to call four different methods that help you adjust schemas to your specific needs: -You also have the option of defining a `JSONSchemaExtend(schema *jsonschema.Schema)` method for your types that will be called _after_ the schema has been generated, allowing you to add or manipulate the fields easily. +- `JSONSchema() *Schema` - will prevent auto-generation of the schema so that you can provide your own definition. +- `JSONSchemaExtend(schema *jsonschema.Schema)` - will be called _after_ the schema has been generated, allowing you to add or manipulate the fields easily. +- `JSONSchemaAlias() any` - is called when reflecting the type of object and allows for an alternative to be used instead. +- `JSONSchemaProperty(prop string) any` - will be called for every property inside a struct giving you the chance to provide an alternative object to convert into a schema. + +Note that all of these methods **must** be defined on a non-pointer object for them to be called. Take the following simplified example of a `CompactDate` that only includes the Year and Month: diff --git a/vendor/github.com/karimkhaleel/jsonschema/comment_extractor.go b/vendor/github.com/karimkhaleel/jsonschema/comment_extractor.go deleted file mode 100644 index e157837af55..00000000000 --- a/vendor/github.com/karimkhaleel/jsonschema/comment_extractor.go +++ /dev/null @@ -1,93 +0,0 @@ -package jsonschema - -import ( - "fmt" - "io/fs" - gopath "path" - "path/filepath" - "strings" - - "go/ast" - "go/doc" - "go/parser" - "go/token" -) - -// ExtractGoComments will read all the go files contained in the provided path, -// including sub-directories, in order to generate a dictionary of comments -// associated with Types and Fields. The results will be added to the `commentsMap` -// provided in the parameters and expected to be used for Schema "description" fields. -// -// The `go/parser` library is used to extract all the comments and unfortunately doesn't -// have a built-in way to determine the fully qualified name of a package. The `base` paremeter, -// the URL used to import that package, is thus required to be able to match reflected types. -// -// When parsing type comments, we use the `go/doc`'s Synopsis method to extract the first phrase -// only. Field comments, which tend to be much shorter, will include everything. -func ExtractGoComments(base, path string, commentMap map[string]string) error { - fset := token.NewFileSet() - dict := make(map[string][]*ast.Package) - err := filepath.Walk(path, func(path string, info fs.FileInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - d, err := parser.ParseDir(fset, path, nil, parser.ParseComments) - if err != nil { - return err - } - for _, v := range d { - // paths may have multiple packages, like for tests - k := gopath.Join(base, path) - dict[k] = append(dict[k], v) - } - } - return nil - }) - if err != nil { - return err - } - - for pkg, p := range dict { - for _, f := range p { - gtxt := "" - typ := "" - ast.Inspect(f, func(n ast.Node) bool { - switch x := n.(type) { - case *ast.TypeSpec: - typ = x.Name.String() - if !ast.IsExported(typ) { - typ = "" - } else { - txt := x.Doc.Text() - if txt == "" && gtxt != "" { - txt = gtxt - gtxt = "" - } - txt = doc.Synopsis(txt) - commentMap[fmt.Sprintf("%s.%s", pkg, typ)] = strings.TrimSpace(txt) - } - case *ast.Field: - txt := x.Doc.Text() - if txt == "" { - txt = x.Comment.Text() - } - if typ != "" && txt != "" { - for _, n := range x.Names { - if ast.IsExported(n.String()) { - k := fmt.Sprintf("%s.%s.%s", pkg, typ, n) - commentMap[k] = strings.TrimSpace(txt) - } - } - } - case *ast.GenDecl: - // remember for the next type - gtxt = x.Doc.Text() - } - return true - }) - } - } - - return nil -} diff --git a/vendor/github.com/karimkhaleel/jsonschema/reflect.go b/vendor/github.com/karimkhaleel/jsonschema/reflect.go index 4c8ff1903bb..a8a7071002a 100644 --- a/vendor/github.com/karimkhaleel/jsonschema/reflect.go +++ b/vendor/github.com/karimkhaleel/jsonschema/reflect.go @@ -15,91 +15,6 @@ import ( "strconv" "strings" "time" - - orderedmap "github.com/wk8/go-ordered-map/v2" -) - -// Version is the JSON Schema version. -var Version = "https://json-schema.org/draft/2020-12/schema" - -// Schema represents a JSON Schema object type. -// RFC draft-bhutton-json-schema-00 section 4.3 -type Schema struct { - // RFC draft-bhutton-json-schema-00 - Version string `json:"$schema,omitempty"` // section 8.1.1 - ID ID `json:"$id,omitempty"` // section 8.2.1 - Anchor string `json:"$anchor,omitempty"` // section 8.2.2 - Ref string `json:"$ref,omitempty"` // section 8.2.3.1 - DynamicRef string `json:"$dynamicRef,omitempty"` // section 8.2.3.2 - Definitions Definitions `json:"$defs,omitempty"` // section 8.2.4 - Comments string `json:"$comment,omitempty"` // section 8.3 - // RFC draft-bhutton-json-schema-00 section 10.2.1 (Sub-schemas with logic) - AllOf []*Schema `json:"allOf,omitempty"` // section 10.2.1.1 - AnyOf []*Schema `json:"anyOf,omitempty"` // section 10.2.1.2 - OneOf []*Schema `json:"oneOf,omitempty"` // section 10.2.1.3 - Not *Schema `json:"not,omitempty"` // section 10.2.1.4 - // RFC draft-bhutton-json-schema-00 section 10.2.2 (Apply sub-schemas conditionally) - If *Schema `json:"if,omitempty"` // section 10.2.2.1 - Then *Schema `json:"then,omitempty"` // section 10.2.2.2 - Else *Schema `json:"else,omitempty"` // section 10.2.2.3 - DependentSchemas map[string]*Schema `json:"dependentSchemas,omitempty"` // section 10.2.2.4 - // RFC draft-bhutton-json-schema-00 section 10.3.1 (arrays) - PrefixItems []*Schema `json:"prefixItems,omitempty"` // section 10.3.1.1 - Items *Schema `json:"items,omitempty"` // section 10.3.1.2 (replaces additionalItems) - Contains *Schema `json:"contains,omitempty"` // section 10.3.1.3 - // RFC draft-bhutton-json-schema-00 section 10.3.2 (sub-schemas) - Properties *orderedmap.OrderedMap[string, *Schema] `json:"properties,omitempty"` // section 10.3.2.1 - OriginalPropertiesMapping map[string]string `json:"-"` - PatternProperties map[string]*Schema `json:"patternProperties,omitempty"` // section 10.3.2.2 - AdditionalProperties *Schema `json:"additionalProperties,omitempty"` // section 10.3.2.3 - PropertyNames *Schema `json:"propertyNames,omitempty"` // section 10.3.2.4 - // RFC draft-bhutton-json-schema-validation-00, section 6 - Type string `json:"type,omitempty"` // section 6.1.1 - Enum []any `json:"enum,omitempty"` // section 6.1.2 - Const any `json:"const,omitempty"` // section 6.1.3 - MultipleOf json.Number `json:"multipleOf,omitempty"` // section 6.2.1 - Maximum json.Number `json:"maximum,omitempty"` // section 6.2.2 - ExclusiveMaximum json.Number `json:"exclusiveMaximum,omitempty"` // section 6.2.3 - Minimum json.Number `json:"minimum,omitempty"` // section 6.2.4 - ExclusiveMinimum json.Number `json:"exclusiveMinimum,omitempty"` // section 6.2.5 - MaxLength int `json:"maxLength,omitempty"` // section 6.3.1 - MinLength int `json:"minLength,omitempty"` // section 6.3.2 - Pattern string `json:"pattern,omitempty"` // section 6.3.3 - MaxItems int `json:"maxItems,omitempty"` // section 6.4.1 - MinItems int `json:"minItems,omitempty"` // section 6.4.2 - UniqueItems bool `json:"uniqueItems,omitempty"` // section 6.4.3 - MaxContains uint `json:"maxContains,omitempty"` // section 6.4.4 - MinContains uint `json:"minContains,omitempty"` // section 6.4.5 - MaxProperties int `json:"maxProperties,omitempty"` // section 6.5.1 - MinProperties int `json:"minProperties,omitempty"` // section 6.5.2 - Required []string `json:"required,omitempty"` // section 6.5.3 - DependentRequired map[string][]string `json:"dependentRequired,omitempty"` // section 6.5.4 - // RFC draft-bhutton-json-schema-validation-00, section 7 - Format string `json:"format,omitempty"` - // RFC draft-bhutton-json-schema-validation-00, section 8 - ContentEncoding string `json:"contentEncoding,omitempty"` // section 8.3 - ContentMediaType string `json:"contentMediaType,omitempty"` // section 8.4 - ContentSchema *Schema `json:"contentSchema,omitempty"` // section 8.5 - // RFC draft-bhutton-json-schema-validation-00, section 9 - Title string `json:"title,omitempty"` // section 9.1 - Description string `json:"description,omitempty"` // section 9.1 - Default any `json:"default,omitempty"` // section 9.2 - Deprecated bool `json:"deprecated,omitempty"` // section 9.3 - ReadOnly bool `json:"readOnly,omitempty"` // section 9.4 - WriteOnly bool `json:"writeOnly,omitempty"` // section 9.4 - Examples []any `json:"examples,omitempty"` // section 9.5 - - Extras map[string]any `json:"-"` - - // Special boolean representation of the Schema - section 4.3.2 - boolean *bool -} - -var ( - // TrueSchema defines a schema with a true value - TrueSchema = &Schema{boolean: &[]bool{true}[0]} - // FalseSchema defines a schema with a false value - FalseSchema = &Schema{boolean: &[]bool{false}[0]} ) // customSchemaImpl is used to detect if the type provides it's own @@ -115,6 +30,22 @@ type extendSchemaImpl interface { JSONSchemaExtend(*Schema) } +// If the object to be reflected defines a `JSONSchemaAlias` method, its type will +// be used instead of the original type. +type aliasSchemaImpl interface { + JSONSchemaAlias() any +} + +// If an object to be reflected defines a `JSONSchemaPropertyAlias` method, +// it will be called for each property to determine if another object +// should be used for the contents. +type propertyAliasSchemaImpl interface { + JSONSchemaProperty(prop string) any +} + +var customAliasSchema = reflect.TypeOf((*aliasSchemaImpl)(nil)).Elem() +var customPropertyAliasSchema = reflect.TypeOf((*propertyAliasSchemaImpl)(nil)).Elem() + var customType = reflect.TypeOf((*customSchemaImpl)(nil)).Elem() var extendType = reflect.TypeOf((*extendSchemaImpl)(nil)).Elem() @@ -209,9 +140,24 @@ type Reflector struct { // If a json tag is present, KeyNamer will receive the tag's name as an argument, not the original key name. KeyNamer func(string) string + // KeyNamerWithOriginalFieldName allows customizing of key names. + // This is like KeyNamer, except it passes in the name it potentially extracted from the json tag as well as + // the original field's name. Has no effect if KeyNamer set. + KeyNamerWithOriginalFieldName func(string, string) string + // AdditionalFields allows adding structfields for a given type AdditionalFields func(reflect.Type) []reflect.StructField + // LookupComment allows customizing comment lookup. Given a reflect.Type and optionally + // a field name, it should return the comment string associated with this type or field. + // + // If the field name is empty, it should return the type's comment; otherwise, the field's + // comment should be returned. If no comment is found, an empty string should be returned. + // + // When set, this function is called before the below CommentMap lookup mechanism. However, + // if it returns an empty string, the CommentMap is still consulted. + LookupComment func(reflect.Type, string) string + // CommentMap is a dictionary of fully qualified go types and fields to comment // strings that will be used if a description has not already been provided in // the tags. Types and fields are added to the package path using "." as a @@ -225,7 +171,7 @@ type Reflector struct { // // map[string]string{"github.com/invopop/jsonschema.Reflector.DoNotReference": "Do not reference definitions."} // - // See also: AddGoComments + // See also: AddGoComments, LookupComment CommentMap map[string]string } @@ -276,11 +222,6 @@ func (r *Reflector) ReflectFromType(t reflect.Type) *Schema { return s } -// Definitions hold schema definitions. -// http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.26 -// RFC draft-wright-json-schema-validation-00, section 5.26 -type Definitions map[string]*Schema - // Available Go defined types for JSON Schema Validation. // RFC draft-wright-json-schema-validation-00, section 7.3 var ( @@ -343,6 +284,15 @@ func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type) return r.refOrReflectTypeToSchema(definitions, t.Elem()) } + // Check if the there is an alias method that provides an object + // that we should use instead of this one. + if t.Implements(customAliasSchema) { + v := reflect.New(t) + o := v.Interface().(aliasSchemaImpl) + t = reflect.TypeOf(o.JSONSchemaAlias()) + return r.refOrReflectTypeToSchema(definitions, t) + } + // Do any pre-definitions exist? if r.Mapper != nil { if t := r.Mapper(t); t != nil { @@ -460,8 +410,9 @@ func (r *Reflector) reflectSliceOrArray(definitions Definitions, t reflect.Type, } if t.Kind() == reflect.Array { - st.MinItems = t.Len() - st.MaxItems = st.MinItems + l := uint64(t.Len()) + st.MinItems = &l + st.MaxItems = &l } if t.Kind() == reflect.Slice && t.Elem() == byteSliceType.Elem() { st.Type = "string" @@ -511,7 +462,6 @@ func (r *Reflector) reflectStruct(definitions Definitions, t reflect.Type, s *Sc r.addDefinition(definitions, t, s) s.Type = "object" s.Properties = NewProperties() - s.OriginalPropertiesMapping = make(map[string]string) s.Description = r.lookupComment(t, "") if r.AssignAnchor { s.Anchor = t.Name() @@ -547,8 +497,17 @@ func (r *Reflector) reflectStructFields(st *Schema, definitions Definitions, t r getFieldDocString = o.GetFieldDocString } + customPropertyMethod := func(string) any { + return nil + } + if t.Implements(customPropertyAliasSchema) { + v := reflect.New(t) + o := v.Interface().(propertyAliasSchemaImpl) + customPropertyMethod = o.JSONSchemaProperty + } + handleField := func(f reflect.StructField) { - name, originalName, shouldEmbed, required, nullable := r.reflectFieldName(f) + name, shouldEmbed, required, nullable := r.reflectFieldName(f) // if anonymous and exported type should be processed recursively // current type should inherit properties of anonymous one if name == "" { @@ -558,7 +517,15 @@ func (r *Reflector) reflectStructFields(st *Schema, definitions Definitions, t r return } - property := r.refOrReflectTypeToSchema(definitions, f.Type) + // If a JSONSchemaAlias(prop string) method is defined, attempt to use + // the provided object's type instead of the field's type. + var property *Schema + if alias := customPropertyMethod(name); alias != nil { + property = r.refOrReflectTypeToSchema(definitions, reflect.TypeOf(alias)) + } else { + property = r.refOrReflectTypeToSchema(definitions, f.Type) + } + property.structKeywordsFromTags(f, st, name) if property.Description == "" { property.Description = r.lookupComment(t, f.Name) @@ -579,7 +546,6 @@ func (r *Reflector) reflectStructFields(st *Schema, definitions Definitions, t r } st.Properties.Set(name, property) - st.OriginalPropertiesMapping[originalName] = name if required { st.Required = appendUniqueString(st.Required, name) } @@ -607,19 +573,6 @@ func appendUniqueString(base []string, value string) []string { return append(base, value) } -func (r *Reflector) lookupComment(t reflect.Type, name string) string { - if r.CommentMap == nil { - return "" - } - - n := fullyQualifiedTypeName(t) - if name != "" { - n = n + "." + name - } - - return r.CommentMap[n] -} - // addDefinition will append the provided schema. If needed, an ID and anchor will also be added. func (r *Reflector) addDefinition(definitions Definitions, t reflect.Type, s *Schema) { name := r.typeName(t) @@ -683,7 +636,7 @@ func (t *Schema) structKeywordsFromTags(f reflect.StructField, parent *Schema, p func (t *Schema) genericKeywords(tags []string, parent *Schema, propertyName string) []string { //nolint:gocyclo unprocessed := make([]string, 0, len(tags)) for _, tag := range tags { - nameValue := strings.Split(tag, "=") + nameValue := strings.SplitN(tag, "=", 2) if len(nameValue) == 2 { name, val := nameValue[0], nameValue[1] switch name { @@ -806,23 +759,18 @@ func (t *Schema) booleanKeywords(tags []string) { // read struct tags for string type keywords func (t *Schema) stringKeywords(tags []string) { for _, tag := range tags { - nameValue := strings.Split(tag, "=") + nameValue := strings.SplitN(tag, "=", 2) if len(nameValue) == 2 { name, val := nameValue[0], nameValue[1] switch name { case "minLength": - i, _ := strconv.Atoi(val) - t.MinLength = i + t.MinLength = parseUint(val) case "maxLength": - i, _ := strconv.Atoi(val) - t.MaxLength = i + t.MaxLength = parseUint(val) case "pattern": t.Pattern = val case "format": - switch val { - case "date-time", "email", "hostname", "ipv4", "ipv6", "uri", "uuid": - t.Format = val - } + t.Format = val case "readOnly": i, _ := strconv.ParseBool(val) t.ReadOnly = i @@ -901,11 +849,9 @@ func (t *Schema) arrayKeywords(tags []string) { name, val := nameValue[0], nameValue[1] switch name { case "minItems": - i, _ := strconv.Atoi(val) - t.MinItems = i + t.MinItems = parseUint(val) case "maxItems": - i, _ := strconv.Atoi(val) - t.MaxItems = i + t.MaxItems = parseUint(val) case "uniqueItems": t.UniqueItems = true case "default": @@ -1029,6 +975,15 @@ func ignoredByJSONSchemaTags(tags []string) bool { return tags[0] == "-" } +func inlinedByJSONTags(tags []string) bool { + for _, tag := range tags[1:] { + if tag == "inline" { + return true + } + } + return false +} + // toJSONNumber converts string to *json.Number. // It'll aso return whether the number is valid. func toJSONNumber(s string) (json.Number, bool) { @@ -1042,6 +997,14 @@ func toJSONNumber(s string) (json.Number, bool) { return json.Number(""), false } +func parseUint(num string) *uint64 { + val, err := strconv.ParseUint(num, 10, 64) + if err != nil { + return nil + } + return &val +} + func (r *Reflector) fieldNameTag() string { if r.FieldNameTag != "" { return r.FieldNameTag @@ -1049,17 +1012,17 @@ func (r *Reflector) fieldNameTag() string { return "json" } -func (r *Reflector) reflectFieldName(f reflect.StructField) (string, string, bool, bool, bool) { +func (r *Reflector) reflectFieldName(f reflect.StructField) (string, bool, bool, bool) { jsonTagString := f.Tag.Get(r.fieldNameTag()) jsonTags := strings.Split(jsonTagString, ",") if ignoredByJSONTags(jsonTags) { - return "", "", false, false, false + return "", false, false, false } schemaTags := strings.Split(f.Tag.Get("jsonschema"), ",") if ignoredByJSONSchemaTags(schemaTags) { - return "", "", false, false, false + return "", false, false, false } var required bool @@ -1073,18 +1036,22 @@ func (r *Reflector) reflectFieldName(f reflect.StructField) (string, string, boo if f.Anonymous && jsonTags[0] == "" { // As per JSON Marshal rules, anonymous structs are inherited if f.Type.Kind() == reflect.Struct { - return "", "", true, false, false + return "", true, false, false } // As per JSON Marshal rules, anonymous pointer to structs are inherited if f.Type.Kind() == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct { - return "", "", true, false, false + return "", true, false, false } } + // As per JSON Marshal rules, inline nested structs that have `inline` tag. + if inlinedByJSONTags(jsonTags) { + return "", true, false, false + } + // Try to determine the name from the different combos name := f.Name - originalName := f.Name if jsonTags[0] != "" { name = jsonTags[0] } @@ -1093,9 +1060,11 @@ func (r *Reflector) reflectFieldName(f reflect.StructField) (string, string, boo name = "" } else if r.KeyNamer != nil { name = r.KeyNamer(name) + } else if r.KeyNamerWithOriginalFieldName != nil { + name = r.KeyNamerWithOriginalFieldName(name, f.Name) } - return name, originalName, false, required, nullable + return name, false, required, nullable } // UnmarshalJSON is used to parse a schema object or boolean. @@ -1133,7 +1102,7 @@ func (t *Schema) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - if t.Extras == nil || len(t.Extras) == 0 { + if len(t.Extras) == 0 { return b, nil } m, err := json.Marshal(t.Extras) @@ -1184,13 +1153,3 @@ func splitOnUnescapedCommas(tagString string) []string { func fullyQualifiedTypeName(t reflect.Type) string { return t.PkgPath() + "." + t.Name() } - -// AddGoComments will update the reflectors comment map with all the comments -// found in the provided source directories. See the #ExtractGoComments method -// for more details. -func (r *Reflector) AddGoComments(base, path string) error { - if r.CommentMap == nil { - r.CommentMap = make(map[string]string) - } - return ExtractGoComments(base, path, r.CommentMap) -} diff --git a/vendor/github.com/karimkhaleel/jsonschema/reflect_comments.go b/vendor/github.com/karimkhaleel/jsonschema/reflect_comments.go new file mode 100644 index 00000000000..ff374c75c80 --- /dev/null +++ b/vendor/github.com/karimkhaleel/jsonschema/reflect_comments.go @@ -0,0 +1,146 @@ +package jsonschema + +import ( + "fmt" + "io/fs" + gopath "path" + "path/filepath" + "reflect" + "strings" + + "go/ast" + "go/doc" + "go/parser" + "go/token" +) + +type commentOptions struct { + fullObjectText bool // use the first sentence only? +} + +// CommentOption allows for special configuration options when preparing Go +// source files for comment extraction. +type CommentOption func(*commentOptions) + +// WithFullComment will configure the comment extraction to process to use an +// object type's full comment text instead of just the synopsis. +func WithFullComment() CommentOption { + return func(o *commentOptions) { + o.fullObjectText = true + } +} + +// AddGoComments will update the reflectors comment map with all the comments +// found in the provided source directories including sub-directories, in order to +// generate a dictionary of comments associated with Types and Fields. The results +// will be added to the `Reflect.CommentMap` ready to use with Schema "description" +// fields. +// +// The `go/parser` library is used to extract all the comments and unfortunately doesn't +// have a built-in way to determine the fully qualified name of a package. The `base` +// parameter, the URL used to import that package, is thus required to be able to match +// reflected types. +// +// When parsing type comments, by default we use the `go/doc`'s Synopsis method to extract +// the first phrase only. Field comments, which tend to be much shorter, will include everything. +// This behavior can be changed by using the `WithFullComment` option. +func (r *Reflector) AddGoComments(base, path string, opts ...CommentOption) error { + if r.CommentMap == nil { + r.CommentMap = make(map[string]string) + } + co := new(commentOptions) + for _, opt := range opts { + opt(co) + } + + return r.extractGoComments(base, path, r.CommentMap, co) +} + +func (r *Reflector) extractGoComments(base, path string, commentMap map[string]string, opts *commentOptions) error { + fset := token.NewFileSet() + dict := make(map[string][]*ast.Package) + err := filepath.Walk(path, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + d, err := parser.ParseDir(fset, path, nil, parser.ParseComments) + if err != nil { + return err + } + for _, v := range d { + // paths may have multiple packages, like for tests + k := gopath.Join(base, path) + dict[k] = append(dict[k], v) + } + } + return nil + }) + if err != nil { + return err + } + + for pkg, p := range dict { + for _, f := range p { + gtxt := "" + typ := "" + ast.Inspect(f, func(n ast.Node) bool { + switch x := n.(type) { + case *ast.TypeSpec: + typ = x.Name.String() + if !ast.IsExported(typ) { + typ = "" + } else { + txt := x.Doc.Text() + if txt == "" && gtxt != "" { + txt = gtxt + gtxt = "" + } + if !opts.fullObjectText { + txt = doc.Synopsis(txt) + } + commentMap[fmt.Sprintf("%s.%s", pkg, typ)] = strings.TrimSpace(txt) + } + case *ast.Field: + txt := x.Doc.Text() + if txt == "" { + txt = x.Comment.Text() + } + if typ != "" && txt != "" { + for _, n := range x.Names { + if ast.IsExported(n.String()) { + k := fmt.Sprintf("%s.%s.%s", pkg, typ, n) + commentMap[k] = strings.TrimSpace(txt) + } + } + } + case *ast.GenDecl: + // remember for the next type + gtxt = x.Doc.Text() + } + return true + }) + } + } + + return nil +} + +func (r *Reflector) lookupComment(t reflect.Type, name string) string { + if r.LookupComment != nil { + if comment := r.LookupComment(t, name); comment != "" { + return comment + } + } + + if r.CommentMap == nil { + return "" + } + + n := fullyQualifiedTypeName(t) + if name != "" { + n = n + "." + name + } + + return r.CommentMap[n] +} diff --git a/vendor/github.com/karimkhaleel/jsonschema/schema.go b/vendor/github.com/karimkhaleel/jsonschema/schema.go new file mode 100644 index 00000000000..2d914b8c835 --- /dev/null +++ b/vendor/github.com/karimkhaleel/jsonschema/schema.go @@ -0,0 +1,94 @@ +package jsonschema + +import ( + "encoding/json" + + orderedmap "github.com/wk8/go-ordered-map/v2" +) + +// Version is the JSON Schema version. +var Version = "https://json-schema.org/draft/2020-12/schema" + +// Schema represents a JSON Schema object type. +// RFC draft-bhutton-json-schema-00 section 4.3 +type Schema struct { + // RFC draft-bhutton-json-schema-00 + Version string `json:"$schema,omitempty"` // section 8.1.1 + ID ID `json:"$id,omitempty"` // section 8.2.1 + Anchor string `json:"$anchor,omitempty"` // section 8.2.2 + Ref string `json:"$ref,omitempty"` // section 8.2.3.1 + DynamicRef string `json:"$dynamicRef,omitempty"` // section 8.2.3.2 + Definitions Definitions `json:"$defs,omitempty"` // section 8.2.4 + Comments string `json:"$comment,omitempty"` // section 8.3 + // RFC draft-bhutton-json-schema-00 section 10.2.1 (Sub-schemas with logic) + AllOf []*Schema `json:"allOf,omitempty"` // section 10.2.1.1 + AnyOf []*Schema `json:"anyOf,omitempty"` // section 10.2.1.2 + OneOf []*Schema `json:"oneOf,omitempty"` // section 10.2.1.3 + Not *Schema `json:"not,omitempty"` // section 10.2.1.4 + // RFC draft-bhutton-json-schema-00 section 10.2.2 (Apply sub-schemas conditionally) + If *Schema `json:"if,omitempty"` // section 10.2.2.1 + Then *Schema `json:"then,omitempty"` // section 10.2.2.2 + Else *Schema `json:"else,omitempty"` // section 10.2.2.3 + DependentSchemas map[string]*Schema `json:"dependentSchemas,omitempty"` // section 10.2.2.4 + // RFC draft-bhutton-json-schema-00 section 10.3.1 (arrays) + PrefixItems []*Schema `json:"prefixItems,omitempty"` // section 10.3.1.1 + Items *Schema `json:"items,omitempty"` // section 10.3.1.2 (replaces additionalItems) + Contains *Schema `json:"contains,omitempty"` // section 10.3.1.3 + // RFC draft-bhutton-json-schema-00 section 10.3.2 (sub-schemas) + Properties *orderedmap.OrderedMap[string, *Schema] `json:"properties,omitempty"` // section 10.3.2.1 + PatternProperties map[string]*Schema `json:"patternProperties,omitempty"` // section 10.3.2.2 + AdditionalProperties *Schema `json:"additionalProperties,omitempty"` // section 10.3.2.3 + PropertyNames *Schema `json:"propertyNames,omitempty"` // section 10.3.2.4 + // RFC draft-bhutton-json-schema-validation-00, section 6 + Type string `json:"type,omitempty"` // section 6.1.1 + Enum []any `json:"enum,omitempty"` // section 6.1.2 + Const any `json:"const,omitempty"` // section 6.1.3 + MultipleOf json.Number `json:"multipleOf,omitempty"` // section 6.2.1 + Maximum json.Number `json:"maximum,omitempty"` // section 6.2.2 + ExclusiveMaximum json.Number `json:"exclusiveMaximum,omitempty"` // section 6.2.3 + Minimum json.Number `json:"minimum,omitempty"` // section 6.2.4 + ExclusiveMinimum json.Number `json:"exclusiveMinimum,omitempty"` // section 6.2.5 + MaxLength *uint64 `json:"maxLength,omitempty"` // section 6.3.1 + MinLength *uint64 `json:"minLength,omitempty"` // section 6.3.2 + Pattern string `json:"pattern,omitempty"` // section 6.3.3 + MaxItems *uint64 `json:"maxItems,omitempty"` // section 6.4.1 + MinItems *uint64 `json:"minItems,omitempty"` // section 6.4.2 + UniqueItems bool `json:"uniqueItems,omitempty"` // section 6.4.3 + MaxContains *uint64 `json:"maxContains,omitempty"` // section 6.4.4 + MinContains *uint64 `json:"minContains,omitempty"` // section 6.4.5 + MaxProperties *uint64 `json:"maxProperties,omitempty"` // section 6.5.1 + MinProperties *uint64 `json:"minProperties,omitempty"` // section 6.5.2 + Required []string `json:"required,omitempty"` // section 6.5.3 + DependentRequired map[string][]string `json:"dependentRequired,omitempty"` // section 6.5.4 + // RFC draft-bhutton-json-schema-validation-00, section 7 + Format string `json:"format,omitempty"` + // RFC draft-bhutton-json-schema-validation-00, section 8 + ContentEncoding string `json:"contentEncoding,omitempty"` // section 8.3 + ContentMediaType string `json:"contentMediaType,omitempty"` // section 8.4 + ContentSchema *Schema `json:"contentSchema,omitempty"` // section 8.5 + // RFC draft-bhutton-json-schema-validation-00, section 9 + Title string `json:"title,omitempty"` // section 9.1 + Description string `json:"description,omitempty"` // section 9.1 + Default any `json:"default,omitempty"` // section 9.2 + Deprecated bool `json:"deprecated,omitempty"` // section 9.3 + ReadOnly bool `json:"readOnly,omitempty"` // section 9.4 + WriteOnly bool `json:"writeOnly,omitempty"` // section 9.4 + Examples []any `json:"examples,omitempty"` // section 9.5 + + Extras map[string]any `json:"-"` + + // Special boolean representation of the Schema - section 4.3.2 + boolean *bool +} + +var ( + // TrueSchema defines a schema with a true value + TrueSchema = &Schema{boolean: &[]bool{true}[0]} + // FalseSchema defines a schema with a false value + FalseSchema = &Schema{boolean: &[]bool{false}[0]} +) + +// Definitions hold schema definitions. +// http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.26 +// RFC draft-wright-json-schema-validation-00, section 5.26 +type Definitions map[string]*Schema diff --git a/vendor/modules.txt b/vendor/modules.txt index c46999dd3b4..172b380b21e 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -184,7 +184,7 @@ github.com/jesseduffield/minimal/gitignore # github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 ## explicit github.com/kardianos/osext -# github.com/karimkhaleel/jsonschema v0.0.0-20231001195015-d933f0d94ea3 +# github.com/karimkhaleel/jsonschema v0.0.0-20250323054317-7eb70f14797b ## explicit; go 1.18 github.com/karimkhaleel/jsonschema # github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd From c233f079ef118643114249df4393ec172ed7d151 Mon Sep 17 00:00:00 2001 From: Karim Khaleel Date: Sun, 23 Mar 2025 10:16:13 -0400 Subject: [PATCH 2/2] fixup! Use keyrenamer function in jsonschema config generation --- pkg/jsonschema/generate.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pkg/jsonschema/generate.go b/pkg/jsonschema/generate.go index ddb23020751..373ea9e3313 100644 --- a/pkg/jsonschema/generate.go +++ b/pkg/jsonschema/generate.go @@ -55,7 +55,6 @@ func customReflect(v *config.UserConfig) *jsonschema.Schema { yamlToFieldNames := make(map[string]string) keyNamer := func(yamlName string, originalFieldName string) string { yamlToFieldNames[yamlName] = originalFieldName - yamlToFieldNames[originalFieldName] = yamlName return yamlName } @@ -76,7 +75,7 @@ func customReflect(v *config.UserConfig) *jsonschema.Schema { subSchema := getSubSchema(schema, userConfigSchema, yamlName) - setDefaultVals(schema, subSchema, defaultValue.FieldByName(fieldName).Interface(), yamlToFieldNames) + setDefaultVals(schema, subSchema, defaultValue.FieldByName(fieldName).Interface(), lo.Invert(yamlToFieldNames)) } return schema @@ -92,7 +91,7 @@ func filterOutDevComments(r *jsonschema.Reflector) { } } -func setDefaultVals(rootSchema, schema *jsonschema.Schema, defaults any, yamlToFieldNames map[string]string) { +func setDefaultVals(rootSchema, schema *jsonschema.Schema, defaults any, fieldToYamlNames map[string]string) { t := reflect.TypeOf(defaults) v := reflect.ValueOf(defaults) @@ -123,7 +122,7 @@ func setDefaultVals(rootSchema, schema *jsonschema.Schema, defaults any, yamlToF value := v.Field(i).Interface() parentKey := t.Field(i).Name - key, ok := yamlToFieldNames[parentKey] + key, ok := fieldToYamlNames[parentKey] if !ok { fmt.Println(key) continue @@ -132,7 +131,7 @@ func setDefaultVals(rootSchema, schema *jsonschema.Schema, defaults any, yamlToF subSchema := getSubSchema(rootSchema, schema, key) if isStruct(value) { - setDefaultVals(rootSchema, subSchema, value, yamlToFieldNames) + setDefaultVals(rootSchema, subSchema, value, fieldToYamlNames) } else if !isZeroValue(value) { subSchema.Default = value }