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
12 changes: 9 additions & 3 deletions compilers/openapi/internal/lowering/promotion.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,12 +204,18 @@ func deprecationField(dep *ir.Deprecation, target ExtensionTarget) *string {
// shape is taken as written — a date is not parsed here, because the mapping is
// the caller's policy and a key it points at the date field is its statement
// that the key holds one.
//
// The target is *string rather than string because JSON null decodes into a
// string without error and leaves it empty, so a bare `x-sunset:` would
// otherwise read as text that says nothing — an empty field written and the
// node marked inferred, with no diagnostic. A key with no value is a value of
// another shape, and is reported as one.
func extensionText(raw ir.RawValue) (string, bool) {
var text string
if err := json.Unmarshal(raw, &text); err != nil {
var text *string
if err := json.Unmarshal(raw, &text); err != nil || text == nil {
return "", false
}
return text, true
return *text, true
}

// extensionOpenness reads a preserved extension value as a statement that an
Expand Down
37 changes: 25 additions & 12 deletions compilers/openapi/internal/lowering/promotion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,18 +142,31 @@ func TestPromoteDeprecation_UndeprecatedNodeIsTheWholeAnswer(t *testing.T) {
// is a reason to leave the field empty and say so, not to coerce.
func TestPromoteDeprecation_ValueThatIsNotTextIsReported(t *testing.T) {
t.Parallel()
unmodeled := ir.Unmodeled{"openapi:x-deprecated-reason": vendorExtension(`7`)}
var dep ir.Deprecation
var prov ir.Provenance
diags := promotionCtx(lowering.ExtensionPromotions{}).PromoteDeprecation(unmodeled, &dep, &prov)

require.Len(t, diags, 1)
assert.Equal(t, ir.SeverityInfo, diags[0].Severity)
assert.Equal(t, "openapi/degraded-construct", diags[0].Code)
assert.Equal(t, "/components/schemas/S", diags[0].Provenance.Pointer,
"the report names the extension rather than the node holding it")
assert.Equal(t, ir.Deprecation{}, dep)
assert.Empty(t, prov.Inferred)
for _, tc := range []struct {
name, value string
}{
{"a number", `7`},
// JSON null decodes into a plain string as "", so this row is what
// separates a bare key from an empty string a document wrote on purpose:
// the first fills nothing and is reported, the second is text.
{"a bare key", `null`},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
unmodeled := ir.Unmodeled{"openapi:x-deprecated-reason": vendorExtension(tc.value)}
var dep ir.Deprecation
var prov ir.Provenance
diags := promotionCtx(lowering.ExtensionPromotions{}).PromoteDeprecation(unmodeled, &dep, &prov)

require.Len(t, diags, 1)
assert.Equal(t, ir.SeverityInfo, diags[0].Severity)
assert.Equal(t, "openapi/degraded-construct", diags[0].Code)
assert.Equal(t, "/components/schemas/S", diags[0].Provenance.Pointer,
"the report names the extension rather than the node holding it")
assert.Equal(t, ir.Deprecation{}, dep)
assert.Empty(t, prov.Inferred)
})
}
}

// TestPromoteDeprecation_MarksOnceBesideWhateverWasAlreadyThere pins the two
Expand Down
Loading