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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ time rather than passing silently:
|---|---|
| `prefixItems` | The array stays a slice of one element type. A tuple has no Go shape a slice can hold. |
| `dependentSchemas` | Not enforced. A property whose shape depends on another is a validation rule, not a type. |
| `if` / `then` / `else` | Not enforced, for the same reason: a shape that depends on a value is not a type. |
| `patternProperties` with several patterns | The map takes an `any` value type, since the patterns disagree about what a key holds. One pattern types the map. |
| `links` | Read and not used. Following a link is a decision for the caller, not a generated method. |
| `mutualTLS` security scheme | No auth provider. The certificate is configured on the `http.Client`. |
Expand Down
2 changes: 2 additions & 0 deletions internal/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type Analyzer struct {
prefixItemsSeen bool
dependentSchemasSeen bool
manyPatternPropertiesSeen bool
conditionalSchemasSeen bool
// warnings collects what the generator had to skip, for the CLI to report.
warnings []string
// linksSeen records that the spec describes response links, which the
Expand Down Expand Up @@ -123,6 +124,7 @@ func (a *Analyzer) Analyze(packageName string) (*ir.Package, error) {
{a.prefixItemsSeen, "prefixItems describes a tuple, which has no Go shape a struct can hold, so those arrays stay slices of one element type"},
{a.dependentSchemasSeen, "dependentSchemas makes a property's shape conditional, which a Go struct cannot express, so it is not enforced"},
{a.manyPatternPropertiesSeen, "patternProperties with more than one pattern disagrees about what a key holds, so those maps take an any value type"},
{a.conditionalSchemasSeen, "if/then/else makes a shape depend on a value, which a Go struct cannot express, so it is not enforced"},
} {
if note.seen {
pkg.Warnings = append(pkg.Warnings, note.text)
Expand Down
28 changes: 28 additions & 0 deletions internal/analyzer/schemas.go
Original file line number Diff line number Diff line change
Expand Up @@ -595,13 +595,35 @@ func (a *Analyzer) convertPrimitive(goName, primaryType string, schema *highbase
// noteUnsupportedKeywords records the JSON Schema keywords the generator reads
// and cannot express in a Go type, so the spec's author hears about it once
// rather than discovering it in the output.
// constGoType returns the Go type a const value implies, for a schema that
// states none of its own.
func constGoType(schema *highbase.Schema) (string, bool) {
if schema.Const == nil {
return "", false
}
switch schema.Const.Tag {
case "!!str":
return "string", true
case "!!int":
return "int64", true
case "!!float":
return "float64", true
case "!!bool":
return "bool", true
}
return "", false
}

func (a *Analyzer) noteUnsupportedKeywords(schema *highbase.Schema) {
if len(schema.PrefixItems) > 0 {
a.prefixItemsSeen = true
}
if schema.DependentSchemas != nil && schema.DependentSchemas.Len() > 0 {
a.dependentSchemasSeen = true
}
if schema.If != nil || schema.Then != nil || schema.Else != nil {
a.conditionalSchemasSeen = true
}
if schema.PatternProperties != nil && schema.PatternProperties.Len() > 1 {
a.manyPatternPropertiesSeen = true
}
Expand Down Expand Up @@ -631,6 +653,12 @@ func (a *Analyzer) resolveGoType(schema *highbase.Schema, nameHint string) strin

// Enum type referenced inline -- use the primary type.
primaryType := primaryType(schema)
if primaryType == "" {
// A const says what the value is, which says what type it has.
if goType, ok := constGoType(schema); ok {
return goType
}
}

switch primaryType {
case "object":
Expand Down
75 changes: 75 additions & 0 deletions internal/analyzer/schemas_const_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package analyzer

import (
"strings"
"testing"
)

const constSpec = `openapi: 3.1.0
info: { title: t, version: "1" }
paths: {}
components:
schemas:
Thing:
type: object
properties:
kind: { const: dog }
count: { const: 3 }
ratio: { const: 1.5 }
enabled: { const: true }
stated: { type: string, const: v2 }
shaped:
const: { a: 1 }
conditional:
type: object
properties: { a: { type: string } }
if: { properties: { a: { const: x } } }
then: { required: [b] }
`

// A const says what the value is, which says what type it has. 3.1 specs pin a
// discriminator this way, and the property was resolving to any.
func TestConst_ImpliesTheType(t *testing.T) {
_, typeMap := analyzeSpec(t, constSpec)

byJSON := map[string]string{}
for _, f := range typeMap["Thing"].Fields {
byJSON[f.JSONName] = f.Type
}

// Optional properties are pointers, as everywhere else.
for prop, want := range map[string]string{
"kind": "*string",
"count": "*int64",
"ratio": "*float64",
"enabled": "*bool",
// A schema that states its own type keeps it.
"stated": "*string",
} {
if byJSON[prop] != want {
t.Errorf("%s = %q, want %q", prop, byJSON[prop], want)
}
}

// A const that is not a scalar says nothing a Go type can carry on its own.
if byJSON["shaped"] != "any" {
t.Errorf("shaped = %q, want any", byJSON["shaped"])
}
}

// if/then/else makes a shape depend on a value, which is a validation rule
// rather than a type, and was dropped without a word while its sibling keyword
// dependentSchemas warned.
func TestConditionalSchemas_Warn(t *testing.T) {
pkg, _ := analyzeSpec(t, constSpec)

var found int
for _, w := range pkg.Warnings {
if strings.Contains(w, "if/then/else") {
found++
}
}
if found != 1 {
t.Errorf("if/then/else warnings = %d, want 1: %v", found, pkg.Warnings)
}
}