diff --git a/pkg/changes/__snapshots__/changes_field_deprecated_test.snap b/pkg/changes/__snapshots__/changes_field_deprecated_test.snap new file mode 100644 index 000000000..823df6120 --- /dev/null +++ b/pkg/changes/__snapshots__/changes_field_deprecated_test.snap @@ -0,0 +1,91 @@ + +[TestChanges_FieldChanges_FieldDeprecated - 1] +# Test: field_deprecated + +## Old Spec: +```yaml +openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: + /widgets: + post: + operationId: updateWidget + requestBody: + content: + application/json: + schema: + type: object + properties: + color: + type: string + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + id: + type: string + nickname: + type: string + label: + type: string +``` + +## New Spec: +```yaml +openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: + /widgets: + post: + operationId: updateWidget + requestBody: + content: + application/json: + schema: + type: object + properties: + color: + type: string + deprecated: true + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + id: + type: string + nickname: + type: string + deprecated: true + label: + type: string + nullable: true + deprecated: true +``` + +## Compact Mode: +## Typescript SDK Changes: +* `sdk.updateWidget()`: + * `request.color` **Deprecated** + * `response` **Changed** (Breaking ⚠️) + +## Full Mode: +## Typescript SDK Changes: +* `sdk.updateWidget()`: + * `request.color` **Deprecated** + * `response` **Changed** (Breaking ⚠️) + - `label` **Changed** (Breaking ⚠️) + - `nickname` **Deprecated** + +--- diff --git a/pkg/changes/__snapshots__/changes_server_sent_events_test.snap b/pkg/changes/__snapshots__/changes_server_sent_events_test.snap new file mode 100644 index 000000000..ac4633ba7 --- /dev/null +++ b/pkg/changes/__snapshots__/changes_server_sent_events_test.snap @@ -0,0 +1,136 @@ + +[TestChanges_MethodChanges_ServerSentEvents - 1] +# Test: server_sent_events + +## Old Spec: +```yaml +openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: + /reports: + post: + operationId: generateReport + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ReportRequest' + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/Report' + text/event-stream: + schema: + $ref: '#/components/schemas/ReportEvent' +components: + schemas: + ReportRequest: + type: object + required: [format] + properties: + format: + type: string + enum: [pdf, csv] + Report: + type: object + properties: + id: + type: string + ReportEvent: + type: object + properties: + data: + type: string +``` + +## New Spec: +```yaml +openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: + /reports: + post: + operationId: generateReport + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ReportRequest' + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/Report' + text/event-stream: + schema: + $ref: '#/components/schemas/ReportEvent' + /widgets: + post: + operationId: createWidget + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/Report' + text/event-stream: + schema: + $ref: '#/components/schemas/ReportEvent' + /events: + get: + operationId: tailEvents + responses: + '200': + description: Success + content: + text/event-stream: + schema: + $ref: '#/components/schemas/ReportEvent' +components: + schemas: + ReportRequest: + type: object + required: [format] + properties: + format: + type: string + enum: [pdf, csv, html] + Report: + type: object + properties: + id: + type: string + ReportEvent: + type: object + properties: + data: + type: string +``` + +## Compact Mode: +## Python SDK Changes: +* `sdk.create_widget()`: **Added** +* `sdk.tail_events()`: **Added** +* `sdk.generate_report()`: + * `request.format.enum(html)` **Added** + +## Full Mode: +## Python SDK Changes: +* `sdk.create_widget()`: **Added** +* `sdk.tail_events()`: **Added** +* `sdk.generate_report()`: + * `request.format.enum(html)` **Added** + +--- diff --git a/pkg/changes/changes.go b/pkg/changes/changes.go index b5bf4ff04..f65e9bd62 100644 --- a/pkg/changes/changes.go +++ b/pkg/changes/changes.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" + generationaccess "github.com/speakeasy-api/generation-context/access" "github.com/speakeasy-api/openapi-generation/v2/internal/analytics" "github.com/speakeasy-api/openapi-generation/v2/internal/ast" "github.com/speakeasy-api/openapi-generation/v2/internal/document" @@ -107,6 +108,11 @@ func resolveAST(ctx context.Context, config GenerateOptions) (*ast.AST, *subsyst if config.Logger == nil { config.Logger = logging.NewLogger(zapcore.DebugLevel) } + // The generator skips operations that use feature-gated constructs, such as + // server-sent event responses, when no generation access state is present. + if _, ok := generationaccess.StateFromContext(ctx); !ok { + ctx = generationaccess.WithDirect(ctx) + } // Check if schema file exists if _, err := os.Stat(config.SchemaPath); err != nil { // If file doesn't exist, return nil AST and nil Subsystem diff --git a/pkg/changes/changes_field_deprecated_test.go b/pkg/changes/changes_field_deprecated_test.go new file mode 100644 index 000000000..0da8fe007 --- /dev/null +++ b/pkg/changes/changes_field_deprecated_test.go @@ -0,0 +1,90 @@ +package changes + +import ( + "testing" + + "github.com/gkampitakis/go-snaps/snaps" +) + +func TestChanges_FieldChanges_FieldDeprecated(t *testing.T) { + oldSpec := `openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: + /widgets: + post: + operationId: updateWidget + requestBody: + content: + application/json: + schema: + type: object + properties: + color: + type: string + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + id: + type: string + nickname: + type: string + label: + type: string` + + newSpec := `openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: + /widgets: + post: + operationId: updateWidget + requestBody: + content: + application/json: + schema: + type: object + properties: + color: + type: string + deprecated: true + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + id: + type: string + nickname: + type: string + deprecated: true + label: + type: string + nullable: true + deprecated: true` + + lang := "typescript" + oldConfig, newConfig := createTestConfigs(t, testSpecPair{ + oldSpec: oldSpec, + newSpec: newSpec, + lang: lang, + }) + + diff, err := Changes(t.Context(), oldConfig, newConfig) + if err != nil { + t.Fatalf("failed to get changes. error: %s", err.Error()) + } + + snapshot := formatTestSnapshotWithDiff("field_deprecated", oldSpec, newSpec, diff) + snaps.MatchSnapshot(t, snapshot) +} diff --git a/pkg/changes/changes_server_sent_events_test.go b/pkg/changes/changes_server_sent_events_test.go new file mode 100644 index 000000000..6d0b759a8 --- /dev/null +++ b/pkg/changes/changes_server_sent_events_test.go @@ -0,0 +1,135 @@ +package changes + +import ( + "testing" + + "github.com/gkampitakis/go-snaps/snaps" +) + +func TestChanges_MethodChanges_ServerSentEvents(t *testing.T) { + oldSpec := `openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: + /reports: + post: + operationId: generateReport + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ReportRequest' + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/Report' + text/event-stream: + schema: + $ref: '#/components/schemas/ReportEvent' +components: + schemas: + ReportRequest: + type: object + required: [format] + properties: + format: + type: string + enum: [pdf, csv] + Report: + type: object + properties: + id: + type: string + ReportEvent: + type: object + properties: + data: + type: string` + + newSpec := `openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: + /reports: + post: + operationId: generateReport + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ReportRequest' + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/Report' + text/event-stream: + schema: + $ref: '#/components/schemas/ReportEvent' + /widgets: + post: + operationId: createWidget + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/Report' + text/event-stream: + schema: + $ref: '#/components/schemas/ReportEvent' + /events: + get: + operationId: tailEvents + responses: + '200': + description: Success + content: + text/event-stream: + schema: + $ref: '#/components/schemas/ReportEvent' +components: + schemas: + ReportRequest: + type: object + required: [format] + properties: + format: + type: string + enum: [pdf, csv, html] + Report: + type: object + properties: + id: + type: string + ReportEvent: + type: object + properties: + data: + type: string` + + lang := "python" + oldConfig, newConfig := createTestConfigs(t, testSpecPair{ + oldSpec: oldSpec, + newSpec: newSpec, + lang: lang, + }) + + diff, err := Changes(t.Context(), oldConfig, newConfig) + if err != nil { + t.Fatalf("failed to get changes. error: %s", err.Error()) + } + + snapshot := formatTestSnapshotWithDiff("server_sent_events", oldSpec, newSpec, diff) + snaps.MatchSnapshot(t, snapshot) +} diff --git a/pkg/changes/diff_typedefs.go b/pkg/changes/diff_typedefs.go index f951e51d4..2f1df43b1 100644 --- a/pkg/changes/diff_typedefs.go +++ b/pkg/changes/diff_typedefs.go @@ -132,13 +132,20 @@ func diffFieldsShallow(params *DiffTypeDefsParams) []shallowDiffResult { // Check field metadata changes equal, breaking := diffFieldDefs(aField, bField, params) diff := TypeDefDiffResult{Equal: true} - if !equal { + switch { + case !equal: diff = TypeDefDiffResult{ Equal: false, Path: childPath, Reason: DiffReasonFieldChanged, IsBreaking: breaking, } + case !isFieldDeprecated(aField) && isFieldDeprecated(bField): + diff = TypeDefDiffResult{ + Equal: false, + Path: childPath, + Reason: DiffReasonFieldDeprecated, + } } // Always create child params for recursion @@ -347,6 +354,10 @@ func diffEnumsShallow(params *DiffTypeDefsParams) []shallowDiffResult { return results } +func isFieldDeprecated(field *ast.FieldDef) bool { + return field.Comments != nil && field.Comments.Deprecated +} + // diffFieldDefs compares two field definitions and returns whether they're equal and if any change is breaking func diffFieldDefs(a, b *ast.FieldDef, params *DiffTypeDefsParams) (equal bool, isBreaking bool) { if a == nil || b == nil { diff --git a/pkg/changes/markdown.go b/pkg/changes/markdown.go index a5a9ef2b0..6e26fd50f 100644 --- a/pkg/changes/markdown.go +++ b/pkg/changes/markdown.go @@ -297,7 +297,7 @@ func formatDiffCompact(result TypeDefDiffResult, prefix string, lang string, det // This is a leaf node - report it if it's a meaningful change if diff.Reason == DiffReasonFieldAdded || diff.Reason == DiffReasonFieldRemoved || - diff.Reason == DiffReasonFieldChanged || diff.Reason == DiffReasonKind || + diff.Reason == DiffReasonFieldChanged || diff.Reason == DiffReasonFieldDeprecated || diff.Reason == DiffReasonKind || diff.Reason == DiffReasonUnionOptionAdded || diff.Reason == DiffReasonUnionOptionRemoved || diff.Reason == DiffReasonUnionOptionChanged || diff.Reason == DiffReasonUnionChanged || diff.Reason == DiffReasonUnionDiscriminatorAdded || @@ -310,17 +310,7 @@ func formatDiffCompact(result TypeDefDiffResult, prefix string, lang string, det } leafPathStr := formatDiffPath(relativePath, lang) - var action string - switch diff.Reason { - case DiffReasonFieldAdded, DiffReasonUnionOptionAdded, DiffReasonEnumValueAdded: - action = "**Added**" - case DiffReasonFieldRemoved, DiffReasonUnionOptionRemoved, DiffReasonEnumValueRemoved: - action = "**Removed**" - default: - action = "**Changed**" - } - - partStr := "`" + leafPathStr + "` " + action + partStr := "`" + leafPathStr + "` " + formatDiffAction(diff.Reason) if diff.IsBreaking { partStr += " (Breaking ⚠️)" } @@ -354,19 +344,8 @@ func formatDiffCompact(result TypeDefDiffResult, prefix string, lang string, det } } - // Generate action based on reason - var action string - switch result.Reason { - case DiffReasonFieldAdded, DiffReasonUnionOptionAdded, DiffReasonEnumValueAdded: - action = "**Added**" - case DiffReasonFieldRemoved, DiffReasonUnionOptionRemoved, DiffReasonEnumValueRemoved: - action = "**Removed**" - default: - action = "**Changed**" - } - result_str := " " + "`" + pathStr + "`" - result_str += " " + action + result_str += " " + formatDiffAction(result.Reason) if result.IsBreaking { result_str += " (Breaking ⚠️)" } @@ -374,6 +353,19 @@ func formatDiffCompact(result TypeDefDiffResult, prefix string, lang string, det return result_str } +func formatDiffAction(reason DiffReason) string { + switch reason { + case DiffReasonFieldAdded, DiffReasonUnionOptionAdded, DiffReasonEnumValueAdded: + return "**Added**" + case DiffReasonFieldRemoved, DiffReasonUnionOptionRemoved, DiffReasonEnumValueRemoved: + return "**Removed**" + case DiffReasonFieldDeprecated: + return "**Deprecated**" + default: + return "**Changed**" + } +} + // formatDiffPath formats a path into a human-readable string, language-specific func formatDiffPath(path []PathSegment, lang string) string { if len(path) == 0 { diff --git a/pkg/changes/types.go b/pkg/changes/types.go index 29ad698e9..b17395eaf 100644 --- a/pkg/changes/types.go +++ b/pkg/changes/types.go @@ -44,6 +44,7 @@ const ( DiffReasonFieldAdded DiffReason = "field_added" // Field was added DiffReasonFieldRemoved DiffReason = "field_removed" // Field was removed DiffReasonFieldChanged DiffReason = "field_changed" // Single field changed + DiffReasonFieldDeprecated DiffReason = "field_deprecated" // Field was marked deprecated DiffReasonFieldsChanged DiffReason = "fields_changed" // Multiple fields changed DiffReasonUnionOptionAdded DiffReason = "union_option_added" // Union option was added DiffReasonUnionOptionRemoved DiffReason = "union_option_removed" // Union option was removed