Skip to content
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
59 changes: 47 additions & 12 deletions cmd/decree/docgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,11 @@ func schemaFromYAML(data []byte) (*docgen.Schema, error) {
return nil, fmt.Errorf("invalid schema YAML: %w", err)
}
s := &docgen.Schema{
Name: sf.Name,
Description: sf.Description,
Version: sf.Version,
Name: sf.Name,
Description: sf.Description,
Version: sf.Version,
VersionDescription: sf.VersionDescription,
Info: schemaInfoFromYAML(sf.Info),
}
for path, f := range sf.Fields {
df := docgen.Field{
Expand All @@ -157,20 +159,53 @@ func schemaFromYAML(data []byte) (*docgen.Schema, error) {
WriteOnce: f.WriteOnce,
Sensitive: f.Sensitive,
}
if len(f.Examples) > 0 {
df.Examples = make(map[string]docgen.FieldExample, len(f.Examples))
for name, ex := range f.Examples {
df.Examples[name] = docgen.FieldExample{Value: ex.Value, Summary: ex.Summary}
}
}
if f.ExternalDocs != nil {
df.ExternalDocs = &docgen.ExternalDocs{
Description: f.ExternalDocs.Description,
URL: f.ExternalDocs.URL,
}
}
if f.Constraints != nil {
df.Constraints = &docgen.Constraints{
Min: f.Constraints.Minimum,
Max: f.Constraints.Maximum,
ExclusiveMin: f.Constraints.ExclusiveMinimum,
ExclusiveMax: f.Constraints.ExclusiveMaximum,
MinLength: f.Constraints.MinLength,
MaxLength: f.Constraints.MaxLength,
Pattern: f.Constraints.Pattern,
Enum: f.Constraints.Enum,
JSONSchema: f.Constraints.JSONSchema,
Min: f.Constraints.Minimum,
Max: f.Constraints.Maximum,
ExclusiveMin: f.Constraints.ExclusiveMinimum,
ExclusiveMax: f.Constraints.ExclusiveMaximum,
MinLength: f.Constraints.MinLength,
MaxLength: f.Constraints.MaxLength,
Pattern: f.Constraints.Pattern,
Enum: f.Constraints.Enum,
JSONSchema: f.Constraints.JSONSchema,
AllowedSchemes: f.Constraints.AllowedSchemes,
}
}
s.Fields = append(s.Fields, df)
}
return s, nil
}

// schemaInfoFromYAML converts the validate info metadata to the docgen shape.
func schemaInfoFromYAML(info *validate.SchemaInfoDef) *docgen.SchemaInfo {
if info == nil {
return nil
}
di := &docgen.SchemaInfo{
Title: info.Title,
Author: info.Author,
Labels: info.Labels,
}
if info.Contact != nil {
di.Contact = &docgen.SchemaContact{
Name: info.Contact.Name,
Email: info.Contact.Email,
URL: info.Contact.URL,
}
}
return di
}
123 changes: 123 additions & 0 deletions cmd/decree/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,129 @@ fields:
}
}

const metadataSchemaYAML = `spec_version: v1
name: payments
version: 3
version_description: Added refund_window field
info:
title: Payments Configuration
author: platform-team
contact:
name: Pat
email: pat@example.com
labels:
team: platform
fields:
payments.fee:
type: number
examples:
low:
value: "0.01"
summary: Low rate
high:
value: "0.99"
externalDocs:
description: Fee guide
url: https://docs.example.com/fees
payments.webhook:
type: url
constraints:
allowed_schemes: [https, sftp]
`

func TestSchemaFromYAML_Metadata(t *testing.T) {
s, err := schemaFromYAML([]byte(metadataSchemaYAML))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := s.VersionDescription; got != "Added refund_window field" {
t.Errorf("got %v, want %v", got, "Added refund_window field")
}
if s.Info == nil {
t.Fatal("expected non-nil Info")
}
want := &docgen.SchemaInfo{
Title: "Payments Configuration",
Author: "platform-team",
Contact: &docgen.SchemaContact{Name: "Pat", Email: "pat@example.com"},
Labels: map[string]string{"team": "platform"},
}
if !reflect.DeepEqual(s.Info, want) {
t.Errorf("got %+v, want %+v", s.Info, want)
}

var fee, webhook *docgen.Field
for i := range s.Fields {
switch s.Fields[i].Path {
case "payments.fee":
fee = &s.Fields[i]
case "payments.webhook":
webhook = &s.Fields[i]
}
}
if fee == nil || webhook == nil {
t.Fatal("expected both fields to be mapped")
}
wantExamples := map[string]docgen.FieldExample{
"low": {Value: "0.01", Summary: "Low rate"},
"high": {Value: "0.99"},
}
if !reflect.DeepEqual(fee.Examples, wantExamples) {
t.Errorf("got %+v, want %+v", fee.Examples, wantExamples)
}
wantDocs := &docgen.ExternalDocs{Description: "Fee guide", URL: "https://docs.example.com/fees"}
if !reflect.DeepEqual(fee.ExternalDocs, wantDocs) {
t.Errorf("got %+v, want %+v", fee.ExternalDocs, wantDocs)
}
if webhook.Constraints == nil || !reflect.DeepEqual(webhook.Constraints.AllowedSchemes, []string{"https", "sftp"}) {
t.Errorf("unexpected constraints: %+v", webhook.Constraints)
}
}

func TestSchemaFromYAML_InfoWithoutContact(t *testing.T) {
yaml := `spec_version: v1
name: test
info:
title: Test
fields:
x:
type: string
`
s, err := schemaFromYAML([]byte(yaml))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if s.Info == nil || s.Info.Title != "Test" {
t.Fatalf("unexpected Info: %+v", s.Info)
}
if s.Info.Contact != nil {
t.Errorf("expected nil Contact, got %+v", s.Info.Contact)
}
}

func TestSchemaFromYAML_MetadataRenders(t *testing.T) {
s, err := schemaFromYAML([]byte(metadataSchemaYAML))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
md := docgen.Generate(*s)
for _, substr := range []string{
"# Payments Configuration",
"**Version:** 3 — Added refund_window field",
"**Author:** platform-team",
"**Contact:** Pat <pat@example.com>",
"`team: platform`",
"- **low:** `0.01` — Low rate",
"- **high:** `0.99`",
"**See also:** [Fee guide](https://docs.example.com/fees)",
"- Allowed schemes: https, sftp",
} {
if !strings.Contains(md, substr) {
t.Errorf("expected output to contain %q, got:\n%s", substr, md)
}
}
}

func TestSchemaFromYAML_Invalid(t *testing.T) {
_, err := schemaFromYAML([]byte("not: [valid"))
if err == nil {
Expand Down
44 changes: 31 additions & 13 deletions sdk/tools/docgen/docgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ type Schema struct {
// Version is the schema version number. A value of 0 means the version is
// not set and will be omitted from the generated documentation.
Version int32
Info *SchemaInfo
Fields []Field
// VersionDescription describes what changed in this version. It annotates
// the version line, so it is only rendered when Version is set.
VersionDescription string
Info *SchemaInfo
Fields []Field
}

// SchemaInfo contains optional schema-level metadata.
Expand Down Expand Up @@ -72,15 +75,16 @@ type ExternalDocs struct {

// Constraints defines validation rules for a field.
type Constraints struct {
Min *float64
Max *float64
ExclusiveMin *float64
ExclusiveMax *float64
MinLength *int32
MaxLength *int32
Pattern string
Enum []string
JSONSchema string
Min *float64
Max *float64
ExclusiveMin *float64
ExclusiveMax *float64
MinLength *int32
MaxLength *int32
Pattern string
Enum []string
JSONSchema string
AllowedSchemes []string
}

// Option configures documentation generation.
Expand Down Expand Up @@ -148,7 +152,11 @@ func Generate(schema Schema, opts ...Option) string {
fmt.Fprintf(&b, "%s\n\n", schema.Description)
}
if schema.Version > 0 {
fmt.Fprintf(&b, "**Version:** %d\n\n", schema.Version)
if schema.VersionDescription != "" {
fmt.Fprintf(&b, "**Version:** %d — %s\n\n", schema.Version, schema.VersionDescription)
} else {
fmt.Fprintf(&b, "**Version:** %d\n\n", schema.Version)
}
}
if schema.Info != nil {
writeSchemaInfo(&b, schema.Info)
Expand Down Expand Up @@ -255,7 +263,14 @@ func writeField(b *strings.Builder, f Field, cfg *config) {
}
if len(f.Examples) > 0 {
fmt.Fprintln(b, "**Examples:**")
for name, ex := range f.Examples {
// Sort names so the output is deterministic across runs.
names := make([]string, 0, len(f.Examples))
for name := range f.Examples {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
ex := f.Examples[name]
if ex.Summary != "" {
fmt.Fprintf(b, "- **%s:** `%s` — %s\n", name, ex.Value, ex.Summary)
} else {
Expand Down Expand Up @@ -309,6 +324,9 @@ func writeConstraints(b *strings.Builder, c *Constraints) {
if c.JSONSchema != "" {
lines = append(lines, "JSON Schema: (see schema definition)")
}
if len(c.AllowedSchemes) > 0 {
lines = append(lines, fmt.Sprintf("Allowed schemes: %s", strings.Join(c.AllowedSchemes, ", ")))
}

if len(lines) > 0 {
fmt.Fprintln(b, "**Constraints:**")
Expand Down
54 changes: 50 additions & 4 deletions sdk/tools/docgen/docgen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,31 @@ func TestGenerate_Basic(t *testing.T) {
assertContains(t, md, "Fee percentage")
}

func TestGenerate_VersionDescription(t *testing.T) {
schema := Schema{
Name: "test",
Version: 3,
VersionDescription: "Added refund_window field",
Fields: []Field{{Path: "x", Type: "string"}},
}

md := Generate(schema)
assertContains(t, md, "**Version:** 3 — Added refund_window field")
}

func TestGenerate_VersionDescriptionWithoutVersion(t *testing.T) {
schema := Schema{
Name: "test",
VersionDescription: "Added refund_window field",
Fields: []Field{{Path: "x", Type: "string"}},
}

md := Generate(schema)
if strings.Contains(md, "Version") || strings.Contains(md, "Added refund_window field") {
t.Errorf("expected no version line when Version is unset, got:\n%s", md)
}
}

func TestGenerate_GroupsByPrefix(t *testing.T) {
schema := Schema{
Name: "test",
Expand Down Expand Up @@ -61,21 +86,43 @@ func TestGenerate_WithoutGrouping(t *testing.T) {
func TestGenerate_Constraints(t *testing.T) {
min := float64(0)
max := float64(100)
exclMin := float64(0)
exclMax := float64(1)
minLen := int32(1)
maxLen := int32(50)
schema := Schema{
Name: "test",
Fields: []Field{
{Path: "rate", Type: "number", Constraints: &Constraints{Min: &min, Max: &max}},
{Path: "name", Type: "string", Constraints: &Constraints{MinLength: &minLen, Pattern: "^[a-z]+$", Enum: []string{"a", "b"}}},
{Path: "share", Type: "number", Constraints: &Constraints{ExclusiveMin: &exclMin, ExclusiveMax: &exclMax}},
{Path: "name", Type: "string", Constraints: &Constraints{MinLength: &minLen, MaxLength: &maxLen, Pattern: "^[a-z]+$", Enum: []string{"a", "b"}}},
{Path: "payload", Type: "json", Constraints: &Constraints{JSONSchema: `{"type": "object"}`}},
},
}

md := Generate(schema)
assertContains(t, md, "Minimum: 0")
assertContains(t, md, "Maximum: 100")
assertContains(t, md, "Exclusive minimum: 0")
assertContains(t, md, "Exclusive maximum: 1")
assertContains(t, md, "Min length: 1")
assertContains(t, md, "Max length: 50")
assertContains(t, md, "Pattern: `^[a-z]+$`")
assertContains(t, md, "Enum: a, b")
assertContains(t, md, "JSON Schema: (see schema definition)")
}

func TestGenerate_AllowedSchemes(t *testing.T) {
schema := Schema{
Name: "test",
Fields: []Field{
{Path: "hook", Type: "url", Constraints: &Constraints{AllowedSchemes: []string{"https", "sftp"}}},
},
}

md := Generate(schema)
assertContains(t, md, "**Constraints:**")
assertContains(t, md, "- Allowed schemes: https, sftp")
}

func TestGenerate_WithoutConstraints(t *testing.T) {
Expand Down Expand Up @@ -250,9 +297,8 @@ func TestGenerate_Examples(t *testing.T) {
}

md := Generate(schema)
assertContains(t, md, "**Examples:**")
assertContains(t, md, "**low:** `0.01` — Low rate")
assertContains(t, md, "**high:** `0.99`")
// Examples render sorted by name for deterministic output.
assertContains(t, md, "**Examples:**\n- **high:** `0.99`\n- **low:** `0.01` — Low rate\n")
}

func TestGenerate_ExternalDocs(t *testing.T) {
Expand Down
Loading
Loading