Skip to content

Commit

Permalink
feature: allow overriding comment tags through aliases
Browse files Browse the repository at this point in the history
Allows more advanced inheritences to be specified for when base Go struct types are shared. A transparent alias can be used to easily apply extra validations
  • Loading branch information
alexzielenski committed Feb 14, 2024
1 parent 0708366 commit bc192d4
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 17 deletions.
108 changes: 92 additions & 16 deletions pkg/generators/markers.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,26 +240,27 @@ func (c commentTags) ValidateType(t *types.Type) error {
// Accepts a prefix to filter out markers not related to validation.
// Returns any errors encountered while parsing or validating the comment tags.
func ParseCommentTags(t *types.Type, comments []string, prefix string) (*spec.Schema, error) {

markers, err := parseMarkers(comments, prefix)
// Parse the comment tags
commentTags, err := parseCommentTagsFromLines(comments, prefix)
if err != nil {
return nil, fmt.Errorf("failed to parse marker comments: %w", err)
}
nested, err := nestMarkers(markers)
if err != nil {
return nil, fmt.Errorf("invalid marker comments: %w", err)
return nil, err
}

// Parse the map into a CommentTags type by marshalling and unmarshalling
// as JSON in leiu of an unstructured converter.
out, err := json.Marshal(nested)
if err != nil {
return nil, fmt.Errorf("failed to marshal marker comments: %w", err)
}
// If t is an alias then parse each of the underlying types' comment tags
// and merge them into a single schema. Aliases closest to t should take
// precedence (e.g. minLength specified in the alias closest to t should override
// any minLength specified in an alias further away from t).
currentT := t
for currentT != nil {
aliasCommentTags, err := parseCommentTagsFromLines(currentT.CommentLines, prefix)
if err != nil {
return nil, err
}

var commentTags commentTags
if err = json.Unmarshal(out, &commentTags); err != nil {
return nil, fmt.Errorf("failed to unmarshal marker comments: %w", err)
mergeCommentTags(&commentTags, aliasCommentTags)

// Move to the next type in the chain
currentT = currentT.Underlying
}

// Validate the parsed comment tags
Expand All @@ -283,6 +284,81 @@ var (
valueRawString = regexp.MustCompile(fmt.Sprintf(`^(%s*)>(.*)$`, allowedKeyCharacterSet))
)

func parseCommentTagsFromLines(comments []string, prefix string) (commentTags, error) {
if len(comments) == 0 {
return commentTags{}, nil
}

markers, err := parseMarkers(comments, prefix)
if err != nil {
return commentTags{}, fmt.Errorf("failed to parse marker comments: %w", err)
}
nested, err := nestMarkers(markers)
if err != nil {
return commentTags{}, fmt.Errorf("invalid marker comments: %w", err)
}

// Parse the map into a CommentTags type by marshalling and unmarshalling
// as JSON in leiu of an unstructured converter.
out, err := json.Marshal(nested)
if err != nil {
return commentTags{}, fmt.Errorf("failed to marshal marker comments: %w", err)
}

var result commentTags
if err = json.Unmarshal(out, &result); err != nil {
return commentTags{}, fmt.Errorf("failed to unmarshal marker comments: %w", err)
}
return result, nil
}

// Writes src values into dst if dst is nil, and src is non-nil
// Does not override anythng already set in dst
func mergeCommentTags(dst *commentTags, src commentTags) {
if src.MinLength != nil && dst.MinLength == nil {
dst.MinLength = src.MinLength
}
if src.MaxLength != nil && dst.MaxLength == nil {
dst.MaxLength = src.MaxLength
}
if src.MinItems != nil && dst.MinItems == nil {
dst.MinItems = src.MinItems
}
if src.MaxItems != nil && dst.MaxItems == nil {
dst.MaxItems = src.MaxItems
}
if src.MinProperties != nil && dst.MinProperties == nil {
dst.MinProperties = src.MinProperties
}
if src.MaxProperties != nil && dst.MaxProperties == nil {
dst.MaxProperties = src.MaxProperties
}
if src.Minimum != nil && dst.Minimum == nil {
dst.Minimum = src.Minimum
}
if src.Maximum != nil && dst.Maximum == nil {
dst.Maximum = src.Maximum
}
if src.MultipleOf != nil && dst.MultipleOf == nil {
dst.MultipleOf = src.MultipleOf
}
if src.Pattern != "" && dst.Pattern == "" {
dst.Pattern = src.Pattern
}
if src.ExclusiveMinimum && !dst.ExclusiveMinimum {
dst.ExclusiveMinimum = true
}
if src.ExclusiveMaximum && !dst.ExclusiveMaximum {
dst.ExclusiveMaximum = true
}
if src.UniqueItems && !dst.UniqueItems {
dst.UniqueItems = true
}
if len(src.CEL) > 0 {
dst.CEL = append(dst.CEL, src.CEL...)
}
}

// extractCommentTags parses comments for lines of the form:
//
// 'marker' + "key=value"
Expand Down
2 changes: 1 addition & 1 deletion pkg/generators/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ func (g openAPITypeWriter) generate(t *types.Type) error {
// Only generate for struct type and ignore the rest
switch t.Kind {
case types.Struct:
validationSchema, err := ParseCommentTags(t, t.CommentLines, markerPrefix)
validationSchema, err := ParseCommentTags(t, nil, markerPrefix)
if err != nil {
return err
}
Expand Down

0 comments on commit bc192d4

Please sign in to comment.