Skip to content

Commit

Permalink
@schema/desc adds description key to OpenAPI Doc (#545)
Browse files Browse the repository at this point in the history
- Reordered the keys so description shows before map and array
  items for readability.
* Add concept of documentation annotations

Co-authored-by: John S. Ryan <[email protected]>
  • Loading branch information
cari-lynn and pivotaljohn authored Nov 17, 2021
1 parent 5c6f293 commit 0c8d4b9
Show file tree
Hide file tree
Showing 7 changed files with 427 additions and 40 deletions.
107 changes: 107 additions & 0 deletions pkg/cmd/template/schema_author_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,113 @@ schema.yml:
})
})
})
t.Run("when schema/desc annotation value", func(t *testing.T) {
t.Run("is empty", func(t *testing.T) {
schemaYAML := `#@data/values-schema
---
#@schema/desc
key: val
`
expectedErr := `
Invalid schema
==============
syntax error in @schema/desc annotation
schema.yml:
|
4 | key: val
|
= found: missing value (in @schema/desc above this item)
= expected: string
`

filesToProcess := files.NewSortedFiles([]*files.File{
files.MustNewFileFromSource(files.NewBytesSource("schema.yml", []byte(schemaYAML))),
})

assertFails(t, filesToProcess, expectedErr, opts)
})
t.Run("has more than one arg", func(t *testing.T) {
schemaYAML := `#@data/values-schema
---
#@schema/desc "two", "strings"
key: val
`
expectedErr := `
Invalid schema
==============
syntax error in @schema/desc annotation
schema.yml:
|
4 | key: val
|
= found: 2 values (in @schema/desc above this item)
= expected: string
`

filesToProcess := files.NewSortedFiles([]*files.File{
files.MustNewFileFromSource(files.NewBytesSource("schema.yml", []byte(schemaYAML))),
})

assertFails(t, filesToProcess, expectedErr, opts)
})
t.Run("is not a string", func(t *testing.T) {
schemaYAML := `#@data/values-schema
---
#@schema/desc 1
key: val
`
expectedErr := `
Invalid schema
==============
syntax error in @schema/desc annotation
schema.yml:
|
4 | key: val
|
= found: Non-string value (in @schema/desc above this item)
= expected: string
`

filesToProcess := files.NewSortedFiles([]*files.File{
files.MustNewFileFromSource(files.NewBytesSource("schema.yml", []byte(schemaYAML))),
})

assertFails(t, filesToProcess, expectedErr, opts)
})
t.Run("is key=value pair", func(t *testing.T) {
schemaYAML := `#@data/values-schema
---
#@schema/desc key=True
key: val
`
expectedErr := `
Invalid schema
==============
syntax error in @schema/desc annotation
schema.yml:
|
4 | key: val
|
= found: keyword argument (in @schema/desc above this item)
= expected: string
= hint: this annotation only accepts one argument: a string.
`

filesToProcess := files.NewSortedFiles([]*files.File{
files.MustNewFileFromSource(files.NewBytesSource("schema.yml", []byte(schemaYAML))),
})

assertFails(t, filesToProcess, expectedErr, opts)
})
})
}

func TestSchema_Provides_default_values(t *testing.T) {
Expand Down
79 changes: 79 additions & 0 deletions pkg/cmd/template/schema_inspect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,85 @@ components:

}

func TestSchemaInspect_annotation_adds_key(t *testing.T) {
t.Run("when description provided by @schema/desc", func(t *testing.T) {
opts := cmdtpl.NewOptions()
opts.DataValuesFlags.InspectSchema = true
opts.RegularFilesSourceOpts.OutputType.Types = []string{"openapi-v3"}

schemaYAML := `#@data/values-schema
#@schema/desc "Network configuration values"
---
#@schema/desc "List of database connections"
db_conn:
#@schema/desc "A network entry"
-
#@schema/desc "The hostname"
hostname: ""
#@schema/desc "Port should be between 49152 through 65535"
port: 0
#@schema/desc "Timeout in minutes"
timeout: 1.0
#@schema/desc "Any type is allowed"
#@schema/type any=True
any_key: thing
#@schema/desc "When not provided, the default is null"
#@schema/nullable
null_key: ""
`
expected := `openapi: 3.0.0
info:
version: 0.1.0
title: Schema for data values, generated by ytt
paths: {}
components:
schemas:
dataValues:
type: object
additionalProperties: false
description: Network configuration values
properties:
db_conn:
type: array
description: List of database connections
items:
type: object
additionalProperties: false
description: A network entry
properties:
hostname:
type: string
default: ""
description: The hostname
port:
type: integer
default: 0
description: Port should be between 49152 through 65535
timeout:
type: number
default: 1
format: float
description: Timeout in minutes
any_key:
nullable: true
default: thing
description: Any type is allowed
null_key:
type: string
default: null
nullable: true
description: When not provided, the default is null
default: []
`

filesToProcess := files.NewSortedFiles([]*files.File{
files.MustNewFileFromSource(files.NewBytesSource("schema.yml", []byte(schemaYAML))),
})

assertSucceedsDocSet(t, filesToProcess, expected, opts)
})
}

func TestSchemaInspect_errors(t *testing.T) {
t.Run("when --output is anything other than 'openapi-v3'", func(t *testing.T) {
opts := cmdtpl.NewOptions()
Expand Down
73 changes: 73 additions & 0 deletions pkg/schema/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const (
AnnotationNullable template.AnnotationName = "schema/nullable"
AnnotationType template.AnnotationName = "schema/type"
AnnotationDefault template.AnnotationName = "schema/default"
AnnotationDescription template.AnnotationName = "schema/desc"
TypeAnnotationKwargAny string = "any"
)

Expand All @@ -36,6 +37,11 @@ type DefaultAnnotation struct {
val interface{}
}

// DescriptionAnnotation documents the purpose of a node
type DescriptionAnnotation struct {
description string
}

// NewTypeAnnotation checks the keyword argument provided via @schema/type annotation, and returns wrapper for the annotated node.
func NewTypeAnnotation(ann template.NodeAnnotation, node yamlmeta.Node) (*TypeAnnotation, error) {
if len(ann.Kwargs) == 0 {
Expand Down Expand Up @@ -128,6 +134,46 @@ func NewDefaultAnnotation(ann template.NodeAnnotation, effectiveType yamlmeta.Ty
return &DefaultAnnotation{yamlmeta.NewASTFromInterfaceWithPosition(val, pos)}, nil
}

// NewDescriptionAnnotation validates the value from the AnnotationDescription, and returns the value
func NewDescriptionAnnotation(ann template.NodeAnnotation, pos *filepos.Position) (*DescriptionAnnotation, error) {
if len(ann.Kwargs) != 0 {
return nil, schemaAssertionError{
position: pos,
description: fmt.Sprintf("syntax error in @%v annotation", AnnotationDescription),
expected: fmt.Sprintf("string"),
found: fmt.Sprintf("keyword argument (in @%v above this item)", AnnotationDescription),
hints: []string{"this annotation only accepts one argument: a string."},
}
}
switch numArgs := len(ann.Args); {
case numArgs == 0:
return nil, schemaAssertionError{
position: pos,
description: fmt.Sprintf("syntax error in @%v annotation", AnnotationDescription),
expected: fmt.Sprintf("string"),
found: fmt.Sprintf("missing value (in @%v above this item)", AnnotationDescription),
}
case numArgs > 1:
return nil, schemaAssertionError{
position: pos,
description: fmt.Sprintf("syntax error in @%v annotation", AnnotationDescription),
expected: fmt.Sprintf("string"),
found: fmt.Sprintf("%v values (in @%v above this item)", numArgs, AnnotationDescription),
}
}

strVal, err := core.NewStarlarkValue(ann.Args[0]).AsString()
if err != nil {
return nil, schemaAssertionError{
position: pos,
description: fmt.Sprintf("syntax error in @%v annotation", AnnotationDescription),
expected: fmt.Sprintf("string"),
found: fmt.Sprintf("Non-string value (in @%v above this item)", AnnotationDescription),
}
}
return &DescriptionAnnotation{strVal}, nil
}

// NewTypeFromAnn returns type information given by annotation.
func (t *TypeAnnotation) NewTypeFromAnn() (yamlmeta.Type, error) {
if t.any {
Expand All @@ -150,6 +196,11 @@ func (n *DefaultAnnotation) NewTypeFromAnn() (yamlmeta.Type, error) {
return nil, nil
}

// NewTypeFromAnn returns type information given by annotation. DescriptionAnnotation has no type information.
func (n *DescriptionAnnotation) NewTypeFromAnn() (yamlmeta.Type, error) {
return nil, nil
}

func (t *TypeAnnotation) IsAny() bool {
return t.any
}
Expand Down Expand Up @@ -189,6 +240,22 @@ func collectValueAnnotations(node yamlmeta.Node, effectiveType yamlmeta.Type) ([
return anns, nil
}

// collectDocumentationAnnotations provides annotations that are used for documentation purposes
func collectDocumentationAnnotations(node yamlmeta.Node) ([]Annotation, error) {
var anns []Annotation

for _, annotation := range []template.AnnotationName{AnnotationDescription} {
ann, err := processOptionalAnnotation(node, annotation, nil)
if err != nil {
return nil, err
}
if ann != nil {
anns = append(anns, ann)
}
}
return anns, nil
}

func processOptionalAnnotation(node yamlmeta.Node, optionalAnnotation template.AnnotationName, effectiveType yamlmeta.Type) (Annotation, error) {
nodeAnnotations := template.NewAnnotations(node)

Expand Down Expand Up @@ -225,6 +292,12 @@ func processOptionalAnnotation(node yamlmeta.Node, optionalAnnotation template.A
return nil, err
}
return defaultAnn, nil
case AnnotationDescription:
descAnn, err := NewDescriptionAnnotation(ann, node.GetPosition())
if err != nil {
return nil, err
}
return descAnn, nil
}
}

Expand Down
Loading

0 comments on commit 0c8d4b9

Please sign in to comment.