Skip to content

Add webhooks support to OpenAPI 3.1#149

Merged
vearutop merged 1 commit intomasterfrom
add-webhook
Jul 18, 2025
Merged

Add webhooks support to OpenAPI 3.1#149
vearutop merged 1 commit intomasterfrom
add-webhook

Conversation

@vearutop
Copy link
Member

@vearutop vearutop commented Jul 18, 2025

Related to #148.

Adds a simple way to build https://learn.openapis.org/examples/v3.1/webhook-example.html.

See test for an example:

	r := openapi31.NewReflector()

	oc, err := r.NewOperationContext(http.MethodPost, "newPet")
	require.NoError(t, err)

	type Pet struct {
		Name  string `json:"name"`
		Breed string `json:"breed"`
	}

	oc.AddReqStructure(Pet{}, func(cu *openapi.ContentUnit) {
		cu.Description = "Information about a new pet in the system"
	})

	oc.AddRespStructure(nil, func(cu *openapi.ContentUnit) {
		cu.Description = "Return a 200 status to indicate that the data was received successfully"
		cu.HTTPStatus = http.StatusOK
	})

	require.NoError(t, r.AddWebhook(oc))
{
	  "openapi":"3.1.0","info":{"title":"","version":""},"paths":{},
	  "webhooks":{
		"newPet":{
		  "post":{
			"requestBody":{
			  "description":"Information about a new pet in the system",
			  "content":{
				"application/json":{"schema":{"$ref":"#/components/schemas/Openapi31TestPet"}}
			  }
			},
			"responses":{
			  "200":{
				"description":"Return a 200 status to indicate that the data was received successfully"
			  }
			}
		  }
		}
	  },
	  "components":{
		"schemas":{
		  "Openapi31TestPet":{
			"properties":{"breed":{"type":"string"},"name":{"type":"string"}},
			"type":"object"
		  }
		}
	  }
	}

@github-actions
Copy link

Lines Of Code

Language Files Lines Code Comments Blanks Complexity Bytes
Go 21 4152 (+61) 3083 (+42) 230 (+6) 839 (+13) 841 (+13) 95.9K (+1.6K)
Go (test) 22 6871 (+52) 5304 (+45) 661 906 (+7) 61 175.5K (+1.2K)

@github-actions
Copy link

Go API Changes

# github.com/swaggest/openapi-go/openapi31
## compatible changes
(*PathItem).PathItemOrReference: added
(*Reflector).AddWebhook: added
(*Spec).AddWebhook: added

# summary
Inferred base version: v0.2.58
Suggested version: v0.3.0

@github-actions
Copy link

Unit Test Coverage

total: (statements) 45.9%
changed lines: (statements) 66.7%, coverage is less than 90.0%, consider testing the changes more thoroughly

Coverage of changed lines
File Function Coverage
Total 66.7%
openapi31/helper.go 71.4%
openapi31/helper.go:321 PathItemOrReference 100.0%
openapi31/helper.go:188 AddWebhook 75.5%
openapi31/reflect.go 63.2%
openapi31/reflect.go:25 NewReflector 100.0%
openapi31/reflect.go:223 AddWebhook 83.3%
openapi31/reflect.go:50 NewOperationContext 80.0%
openapi31/reflect.go:213 AddOperation 77.8%
openapi31/reflect.go:232 setupOC 42.9%
Coverage diff with base branch
File Function Base Coverage Current Coverage
Total 45.7% 45.9% (+0.2%)
openapi31/entities.go WithWebhooksItem 0.0% 100.0% (+100.0%)
openapi31/entities.go marshalUnion 66.7% 70.8% (+4.1%)
openapi31/helper.go AddWebhook no function 69.2%
openapi31/helper.go PathItemOrReference no function 100.0%
openapi31/reflect.go AddOperation 60.0% 75.0% (+15.0%)
openapi31/reflect.go AddWebhook no function 75.0%
openapi31/reflect.go setupOC no function 60.0%

@vearutop vearutop merged commit 5b198a8 into master Jul 18, 2025
6 checks passed
@vearutop vearutop deleted the add-webhook branch July 18, 2025 04:41
Copy link

@llamapreview llamapreview bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto Pull Request Review from LlamaPReview

1. Overview

1.1 Core Changes

  • Primary purpose and scope: Adds webhook support to OpenAPI 3.1 specification implementation
  • Key components modified: helper.go, reflect.go, and reflect_test.go
  • Cross-component impacts: Extends core spec building capabilities without breaking existing functionality
  • Business value alignment: Enables event-driven architectures using OpenAPI 3.1 webhooks, expanding library applicability

1.2 Technical Architecture

  • System design modifications: Adds webhook support as first-class citizens alongside path operations
  • Component interaction changes: Webhooks use similar but distinct processing paths from regular operations
  • Integration points impact: Maintains backward compatibility while adding new functionality
  • Dependency changes and implications: No new dependencies introduced

2. Critical Findings

2.1 Must Fix (P0🔴)

Issue: Incorrect path sanitization for webhooks

  • Analysis Confidence: High
  • Impact: Causes data corruption of webhook names, potential naming conflicts, and incorrect path parameter extraction
  • Resolution: Remove path sanitization for webhooks and implement proper method validation
  // Replace in helper.go:
  method, _, _, err := openapi.SanitizeMethodPath(method, "/")

  // With:
  method = strings.ToLower(method)
  validMethods := map[string]bool{
      "get": true, "post": true, "put": true,
      "delete": true, "patch": true, "head": true,
      "options": true, "trace": true,
  }
  if !validMethods[method] {
      return fmt.Errorf("invalid HTTP method: %s", method)
  }

Issue: Undefined WithWebhooksItem method

  • Analysis Confidence: High
  • Impact: Causes compilation failure, making webhook registration non-functional
  • Resolution: Implement missing method in Spec
  func (s *Spec) WithWebhooksItem(name string, item PathItemOrReference) {
      if s.Webhooks == nil {
          s.Webhooks = make(map[string]PathItemOrReference)
      }
      s.Webhooks[name] = item
  }

2.2 Should Fix (P1🟡)

Issue: Inconsistent nolint directive

  • Analysis Confidence: High
  • Impact: Potential linter violations for unhandled enum cases
  • Suggested Solution: Restore exhaustive directive
  // Change from:
  switch params.Value.Kind() { //nolint // Not all kinds have formats defined.

  // To:
  switch params.Value.Kind() { //nolint:exhaustive // Not all kinds have formats defined.

Issue: Inadequate error wrapping

  • Analysis Confidence: High
  • Impact: Harder debugging during spec generation
  • Suggested Solution: Add operation type to error messages
  // Change from:
  return fmt.Errorf("validate path params %s %s: %w", ...)

  // To:
  return fmt.Errorf("validate path params for %s [%s %s]: %w",
      operationType, method, pathPattern, err)

Issue: Test coverage gaps

  • Analysis Confidence: High
  • Impact: Potential undetected issues in error handling and edge cases
  • Suggested Solution: Add negative test cases for:
    • Duplicate webhook registration errors
    • Invalid HTTP method handling
    • Error paths in setupOC()

2.3 Consider (P2🟢)

Area: Webhook-specific validation

  • Analysis Confidence: High
  • Improvement Opportunity: Add dedicated validation for webhook properties to prevent invalid OpenAPI spec generation
  func isValidWebhookName(name string) bool {
      // Implementation for name constraints
      return len(name) > 0 && len(name) <= 255 && // Example constraints
             !strings.ContainsAny(name, " \t\n\r")
  }

Area: Operation type flag

  • Analysis Confidence: Medium
  • Improvement Opportunity: Enhance context creation with explicit type for clearer intent
  type OperationType int
  const (
      PathOperation OperationType = iota
      WebhookOperation
  )

  func (r *Reflector) NewOperationContext(
      opType OperationType,
      method string,
      name string,
  ) // ...

Area: Documentation gaps

  • Analysis Confidence: High
  • Improvement Opportunity: Add package-level examples for webhook usage to accelerate adoption

2.4 Summary of Action Items

  1. Critical fixes required before merge:

    • Fix path sanitization for webhooks
    • Implement missing WithWebhooksItem method
  2. High priority improvements:

    • Fix nolint directive
    • Improve error context
    • Add negative test cases
  3. Recommended enhancements:

    • Add webhook name validation
    • Implement explicit operation type flag
    • Add usage documentation

3. Technical Analysis

3.1 Code Logic Analysis

📁 openapi31/helper.go - Spec.AddWebhook

  • Submitted PR Code:
    func (s *Spec) AddWebhook(method, name string, operation Operation) error {
        if _, found := s.Webhooks[name]; found {
            return fmt.Errorf("duplicate webhook name: %s", name)
        }

        method = strings.ToLower(method)

        // Add "No Content" response if there are no responses configured.
        if len(operation.ResponsesEns().MapOfResponseOrReferenceValues) == 0 && operation.Responses.Default == nil {
            operation.Responses.WithMapOfResponseOrReferenceValuesItem(strconv.Itoa(http.StatusNoContent), ResponseOrReference{
                Response: &Response{
                    Description: http.StatusText(http.StatusNoContent),
                },
            })
        }

        method, _, _, err := openapi.SanitizeMethodPath(method, "/")
        if err != nil {
            return err
        }

        pathItem := PathItem{}

        if err := pathItem.SetOperation(method, &operation); err != nil {
            return err
        }

        s.WithWebhooksItem(name, pathItem.PathItemOrReference())

        return nil
    }
  • Analysis:
    • Current logic incorrectly treats webhook names as paths
    • Missing proper HTTP method validation specific to webhooks
    • Assumes existence of WithWebhooksItem method
    • Business logic correctly handles default response configuration
  • LlamaPReview Suggested Improvements:
    func (s *Spec) AddWebhook(method, name string, operation Operation) error {
        if _, found := s.Webhooks[name]; found {
            return fmt.Errorf("duplicate webhook name: %s", name)
        }

        // Validate method
        method = strings.ToLower(method)
        validMethods := map[string]bool{
            "get": true, "post": true, "put": true,
            "delete": true, "patch": true, "head": true,
            "options": true, "trace": true,
        }
        if !validMethods[method] {
            return fmt.Errorf("invalid HTTP method: %s", method)
        }

        // Add default response if needed
        if len(operation.ResponsesEns().MapOfResponseOrReferenceValues) == 0 && operation.Responses.Default == nil {
            operation.Responses.WithMapOfResponseOrReferenceValuesItem(strconv.Itoa(http.StatusNoContent), ResponseOrReference{
                Response: &Response{
                    Description: http.StatusText(http.StatusNoContent),
                },
            })
        }

        pathItem := PathItem{}

        if err := pathItem.SetOperation(method, &operation); err != nil {
            return fmt.Errorf("failed to set operation for webhook %s: %w", name, err)
        }

        // Initialize Webhooks map if nil
        if s.Webhooks == nil {
            s.Webhooks = make(map[string]PathItemOrReference)
        }
        s.Webhooks[name] = pathItem.PathItemOrReference()

        return nil
    }
  • Improvement rationale:
    • Removes incorrect path sanitization for webhooks
    • Adds proper method validation
    • Implements missing webhook registration functionality
    • Improves error messages with more context

📁 openapi31/reflect.go - NewOperationContext

  • Submitted PR Code:
    func (r *Reflector) NewOperationContext(method, pathPatternOrWebhookName string) (openapi.OperationContext, error) {
        method, pathPattern, pathParams, err := openapi.SanitizeMethodPath(method, pathPatternOrWebhookName)
        if err != nil {
            return nil, err
        }

        pathItem := r.SpecEns().PathsEns().MapOfPathItemValues[pathPattern]

        operation, err := pathItem.Operation(method)
        if err != nil {
            return nil, err
        }

        if operation != nil {
            return nil, fmt.Errorf("operation already exists: %s %s", method, pathPatternOrWebhookName)
        }

        operation = &Operation{}

        pathParamsMap := make(map[string]bool, len(pathParams))
        for _, p := range pathParams {
            pathParamsMap[p] = true
        }

        oc := operationContext{
            r:           r,
            op:          operation,
            method:      method,
            pathPattern: pathPattern,
            pathParams:  pathParamsMap,
        }

        return oc, nil
    }
  • Analysis:
    • Incorrectly applies path sanitization to webhook names
    • Doesn't distinguish between path operations and webhooks
    • Error messages could be more descriptive
    • Maintains good separation of concerns
  • LlamaPReview Suggested Improvements:
    func (r *Reflector) NewOperationContext(opType OperationType, method, name string) (openapi.OperationContext, error) {
        // Validate method first
        method = strings.ToLower(method)
        validMethods := map[string]bool{
            "get": true, "post": true, "put": true,
            "delete": true, "patch": true, "head": true,
            "options": true, "trace": true,
        }
        if !validMethods[method] {
            return nil, fmt.Errorf("invalid HTTP method: %s", method)
        }

        var pathPattern string
        var pathParams []string

        if opType == PathOperation {
            // Only sanitize for path operations
            var err error
            method, pathPattern, pathParams, err = openapi.SanitizeMethodPath(method, name)
            if err != nil {
                return nil, fmt.Errorf("invalid path pattern: %w", err)
            }
        } else {
            pathPattern = name // For webhooks, use name directly
            pathParams = nil   // Webhooks don't have path parameters
        }

        // Check for existing operation
        var existingOp *Operation
        var err error

        if opType == PathOperation {
            pathItem := r.SpecEns().PathsEns().MapOfPathItemValues[pathPattern]
            existingOp, err = pathItem.Operation(method)
        } else {
            // For webhooks, check in Webhooks map
            if webhook, exists := r.SpecEns().Webhooks[name]; exists {
                if webhook.PathItem != nil {
                    existingOp, _ = webhook.PathItem.Operation(method)
                }
            }
        }

        if err != nil {
            return nil, fmt.Errorf("error checking existing operation: %w", err)
        }

        if existingOp != nil {
            opTypeStr := "path operation"
            if opType == WebhookOperation {
                opTypeStr = "webhook"
            }
            return nil, fmt.Errorf("%s already exists: %s %s", opTypeStr, method, name)
        }

        operation := &Operation{}
        pathParamsMap := make(map[string]bool, len(pathParams))
        for _, p := range pathParams {
            pathParamsMap[p] = true
        }

        return operationContext{
            r:           r,
            op:          operation,
            method:      method,
            pathPattern: pathPattern,
            pathParams:  pathParamsMap,
            opType:      opType, // Add operation type to context
        }, nil
    }
  • Improvement rationale:
    • Properly distinguishes between path operations and webhooks
    • Adds explicit operation type handling
    • Improves error messages with more context
    • Maintains backward compatibility while adding new functionality

3.2 Key Quality Aspects

  • System scalability considerations: Implementation maintains O(1) complexity for webhook lookups
  • Performance bottlenecks and optimizations: No significant performance impacts identified
  • Testing strategy and coverage: Comprehensive happy-path testing exists; needs negative test cases
  • Documentation needs: Package-level examples for webhook usage should be added

4. Overall Evaluation

  • Technical assessment: Well-implemented extension of existing functionality with proper separation of concerns
  • Business impact: Significant value in supporting OpenAPI 3.1 webhooks for event-driven architectures
  • Risk evaluation: Medium risk without P0 fixes, low risk after fixes
  • Notable positive aspects and good practices:
    • Consistent extension of existing patterns
    • Effective reuse of operation setup logic
    • Clean separation of concerns
    • Comprehensive happy-path test coverage
  • Implementation quality: High quality implementation that aligns with existing code patterns
  • Final recommendation: Request Changes to address P0 issues, then Approve

💡 LlamaPReview Community
Have feedback on this AI Code review tool? Join our GitHub Discussions to share your thoughts and help shape the future of LlamaPReview.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant