Skip to content

Latest commit

 

History

History
687 lines (478 loc) · 17.4 KB

File metadata and controls

687 lines (478 loc) · 17.4 KB

Contributing to oastools

Thank you for your interest in contributing to oastools! This document provides everything you need to know to contribute effectively.

Table of Contents

Quick Start

Prerequisites

  • Go 1.25+ - Required for development
  • golangci-lint - For linting (optional but recommended)
  • gotestsum - For better test output formatting (optional)

Clone and Build

# Clone the repository
git clone https://github.com/erraggy/oastools.git
cd oastools

# Install dependencies
make deps

# Build the binary
make build

# Run tests
make test

# Run all quality checks
make check

Development Workflow

The Golden Rule: Always Run make check

After making changes, always run:

make check

This command runs:

  1. go mod tidy - Clean up dependencies
  2. go fmt - Format code
  3. golangci-lint run - Lint code
  4. go test with race detection - Run tests
  5. git status - Show what changed

Common Development Commands

# Build the binary (outputs to bin/oastools)
make build

# Install to $GOPATH/bin
make install

# Run tests with coverage
make test

# Generate HTML coverage report
make test-coverage

# Format code
make fmt

# Run linter
make lint

# Clean build artifacts
make clean

Development Loop

  1. Make your changes
  2. Run make check to validate
  3. Fix any issues reported
  4. Commit your changes
  5. Create a pull request

Project Architecture

What is oastools?

oastools is a Go-based CLI tool and library for working with OpenAPI Specification (OAS) files. It provides:

  • Validation - Ensure OAS files conform to specifications
  • Parsing - Load and analyze OAS documents
  • Joining - Combine multiple OAS files
  • Converting - Transform between OAS versions (2.0 ↔ 3.x)
  • Diffing - Compare specs and detect breaking changes

Directory Structure

oastools/
├── cmd/oastools/       # CLI entry point
│   └── main.go         # Command dispatcher
├── parser/             # Parse OAS files (public API)
├── validator/          # Validate OAS files (public API)
├── joiner/             # Join multiple OAS files (public API)
├── converter/          # Convert between OAS versions (public API)
├── differ/             # Compare OAS files (public API)
├── internal/           # Internal utilities (not public API)
│   ├── httputil/       # HTTP constants and validation
│   ├── severity/       # Issue severity levels
│   ├── issues/         # Unified issue reporting
│   └── testutil/       # Test helpers
└── testdata/           # Test fixtures

Public vs Internal Packages

Public packages (can be imported by external projects):

  • parser - Parse OpenAPI specifications
  • validator - Validate OpenAPI specifications
  • joiner - Join multiple specifications
  • converter - Convert between versions
  • differ - Compare specifications

Internal packages (project-only):

  • internal/* - Shared utilities not exposed to external users

Design Principles

  1. Public API First - Core functionality is exposed as importable Go packages
  2. Separation of Concerns - Each package has one responsibility
  3. Format Preservation - Input format (JSON/YAML) is automatically preserved
  4. Comprehensive Documentation - Every public package has doc.go and example_test.go
  5. Testability - High test coverage required for all exported functionality

Code Standards

Go Style Guidelines

  • Follow standard Go conventions
  • Use gofmt for formatting (run via make fmt)
  • Pass golangci-lint checks (run via make lint)
  • Use meaningful variable and function names
  • Write self-documenting code; add comments only where logic isn't obvious

Documentation Requirements

All exported functionality must have:

  1. Godoc comments - Describe what it does
  2. Package-level docs - Update doc.go if adding new public APIs
  3. Runnable examples - Add to example_test.go for new features

Example:

// Parse parses an OpenAPI specification file from the given path.
// It automatically detects the file format (JSON or YAML) and validates
// the document structure if validateStructure is true.
func Parse(specPath string, resolveRefs bool, validateStructure bool) (*ParseResult, error) {
    // Implementation
}

Constant Usage

Always use package-level constants instead of string literals:

// ❌ Bad - hardcoded strings
if method == "get" { ... }

// ✅ Good - use constants
if method == httputil.MethodGet { ... }

This ensures:

  • Single source of truth
  • Type safety
  • Easy refactoring
  • Clear intent

Format Preservation

IMPORTANT: The parser, converter, and joiner automatically preserve input file format.

  • Input JSON → Output JSON
  • Input YAML → Output YAML

This is handled automatically via the SourceFormat field in ParseResult. When writing new features:

  1. Don't manually choose output format
  2. Do use result.SourceFormat to determine marshaling
  3. Test both JSON and YAML format preservation

Error Handling

  • Return errors; don't panic (except for programmer errors)
  • Use fmt.Errorf with %w for error wrapping
  • Provide context in error messages
if err != nil {
    return nil, fmt.Errorf("failed to parse spec at %s: %w", specPath, err)
}

Testing Requirements

Coverage Expectations

All exported functionality MUST have comprehensive test coverage.

This includes:

  • ✅ Exported functions (e.g., parser.Parse())
  • ✅ Exported methods (e.g., Parser.Parse())
  • ✅ Exported types and their fields
  • ✅ Exported constants and variables

Test Types Required

  1. Positive tests - Valid inputs produce expected outputs
  2. Negative tests - Invalid inputs produce appropriate errors
  3. Edge cases - Boundary conditions, empty inputs, nil values
  4. Integration tests - Multiple components working together

Test Naming Convention

// Package-level convenience functions
func TestParseConvenience(t *testing.T) { ... }

// Struct methods
func TestParserParse(t *testing.T) { ... }

// Specific features
func TestJSONFormatPreservation(t *testing.T) { ... }

Benchmark Tests

Use the Go 1.24+ for b.Loop() pattern:

func BenchmarkParse(b *testing.B) {
    // Setup
    specPath := "testdata/petstore.yaml"

    // Benchmark loop
    for b.Loop() {
        _, err := Parse(specPath, false, true)
        if err != nil {
            b.Fatal(err)
        }
    }
}

Don't use:

  • for i := 0; i < b.N; i++ (old pattern)
  • b.ReportAllocs() (handled automatically by b.Loop())

Running Tests

# Run all tests
make test

# Run tests with coverage
make test-coverage

# Run specific package tests
go test ./parser/...

# Run specific test
go test ./parser -run TestParse

# Run benchmarks
go test -bench=. ./parser

Submitting Changes

Before You Commit

  1. ✅ Run make check and ensure it passes
  2. ✅ Add tests for new functionality
  3. ✅ Update documentation (godoc, doc.go, example_test.go)
  4. ✅ Verify test coverage is sufficient
  5. ✅ Update benchmarks with make bench-save for performance-impacting changes

Documentation Checks

CI automatically verifies that documentation stays in sync with code through automated checks:

  • CLI flag tables (TestCLIFlagsDocumented) - CLI flags must match docs/cli-reference.md
  • Option tables (TestDeepDiveOptionTables) - With* functions must appear in respective deep_dive.md
  • Link checking (lychee) - Broken links caught in CI and via make docs-check

If you add a CLI flag or With* option, the tests will tell you which doc to update.

Commit Message Format

Use conventional commit format:

<type>(<scope>): <subject>

<body>

<footer>

Types:

  • feat - New feature
  • fix - Bug fix
  • docs - Documentation only
  • test - Adding/updating tests
  • refactor - Code restructuring (no behavior change)
  • perf - Performance improvements
  • chore - Maintenance tasks

Examples:

feat(parser): add support for OAS 3.2.0

Implemented parsing logic for the new OAS 3.2.0 specification,
including support for the updated JSON Schema Draft 2020-12
alignment and new spec features.

- Added version detection for 3.2.0
- Updated schema validation
- Added test fixtures for 3.2.0
fix(converter): handle nullable types in OAS 3.1 conversion

Fixed conversion of OAS 3.1 nullable types that use type arrays
instead of the deprecated nullable field.

Fixes #123
chore: run go mod tidy

[skip-review] - automated dependency cleanup

Skipping Automated Code Review

You can skip the Claude Code Review workflow for trivial commits by adding [skip-review] to your commit message:

# Example: Skip review for automated formatting
git commit -m "chore: run go fmt

[skip-review] - automated code formatting, no logic changes"

# Example: Skip review for dependency updates
git commit -m "chore: update dependencies

[skip-review] - go mod tidy only"

When to use [skip-review]:

  • Automated formatting (go fmt, gofmt)
  • Dependency updates (go mod tidy)
  • Minor documentation typos
  • Whitespace or comment-only changes

When NOT to use [skip-review]:

  • Any logic changes
  • New features
  • Bug fixes
  • Refactoring
  • Test additions/changes

The review will be skipped if any commit in your PR contains [skip-review].

Pull Request Process

  1. Create a feature branch

    git checkout -b feature/your-feature-name
  2. Make your changes and commit

    # Make changes
    make check
    git add .
    git commit -m "feat(scope): description"
  3. Push to your fork

    git push origin feature/your-feature-name
  4. Create a Pull Request

    • Use a clear, descriptive title
    • Reference any related issues
    • Describe what changed and why
    • Include testing instructions if applicable
  5. Address Review Feedback

    • Respond to comments
    • Make requested changes
    • Push updates to your branch
    • Request re-review when ready

PR Review Checklist

Before requesting review, ensure:

  • make check passes
  • All tests pass with make test
  • New functionality has tests
  • Public APIs have godoc comments
  • Examples added for new features
  • Benchmarks updated with make bench-save (if changes affect performance)
  • No unintended files committed (e.g., binaries, editor files)
  • Commit messages follow conventional format

CI/CD and Automation

Automated Workflows

When you create a PR, several automated workflows run:

  1. Go Tests - Runs test suite across multiple Go versions
  2. golangci-lint - Lints code for issues
  3. Claude Code Review (optional) - AI-powered code review
    • Skipped if [skip-review] in any commit message
    • Provides feedback on code quality, bugs, performance, security

Workflow Status

Check workflow status:

Common CI Issues

Tests fail on CI but pass locally:

  • Ensure you're testing with race detection: go test -race
  • Check for timing-dependent tests
  • Verify all test files are committed

Exit code 143 (SIGTERM):

  • This means the test process was killed by the runner
  • Common with go test -race on GitHub Actions
  • Usually indicates tests hung or timed out
  • Current mitigations in place:
    • Test timeout: 10 minutes per package
    • Job timeout: 15 minutes total
    • Limited parallelism: -parallel=4
    • GOMAXPROCS=2 to prevent resource exhaustion
  • If you see this error intermittently, it's likely a runner resource issue, not your code
  • Related: actions/runner-images#6680, actions/runner-images#7146

Linter fails:

  • Run make lint locally
  • Fix reported issues
  • Push fixes

Claude Code Review comments:

  • Review the feedback (visible in PR comments)
  • Address legitimate concerns
  • Respond to questions
  • Push updates if needed

Key OpenAPI Concepts

Supported OAS Versions

oastools supports all major OpenAPI Specification versions:

All versions use JSON Schema Draft 2020-12 for schema definitions.

Version Evolution

Understanding how OAS evolved helps when working with conversion and validation:

OAS 2.0 → 3.0 Changes:

  • host/basePath/schemes → unified servers array
  • definitions/parameters/responsescomponents.*
  • consumes + body param → requestBody.content
  • produces + schema → responses.*.content
  • Added: callbacks, links, cookie parameters

OAS 3.0 → 3.1 Changes:

  • Full JSON Schema alignment
  • type can be array: ["string", "null"]
  • Deprecated nullable field
  • Added webhooks for event-driven APIs
  • Added license.identifier

Critical Type Handling

Be careful with interface{} fields:

Some OAS 3.1+ fields accept multiple types and are defined as interface{}:

// schema.Type can be string OR []string
if typeStr, ok := schema.Type.(string); ok {
    // Single type: "string"
} else if typeArr, ok := schema.Type.([]string); ok {
    // Multiple types: ["string", "null"]
}

Always use type assertions - never assume the type!

Version-Specific Features

OAS 2.0 Only:

  • allowEmptyValue (removed in 3.0+)
  • collectionFormat (replaced by style/explode)

OAS 3.0+ Only:

  • requestBody (replaces body parameters)
  • callbacks (async operations)
  • links (operation relationships)
  • Cookie parameters (in: cookie)
  • TRACE HTTP method

OAS 3.1+ Only:

  • webhooks (event subscriptions)
  • Type arrays for nullable
  • license.identifier

When working with conversions, these differences matter!

Getting Help

Resources

OpenAPI Specifications

Asking Questions

  • Open a GitHub Issue for bugs or feature requests
  • Start a GitHub Discussion for questions
  • Check existing issues/discussions first - your question may already be answered

Before Filing an Issue

  1. Search existing issues

  2. Provide a minimal reproduction case

  3. Include relevant version information:

    oastools --version
    go version
  4. Include error messages and stack traces

  5. Describe expected vs actual behavior

Common Pitfalls

Type Assertions

Problem: Assuming schema.Type is always a string

// ❌ Wrong - will panic if Type is []string
typeStr := schema.Type.(string)

// ✅ Correct - safe type assertion
if typeStr, ok := schema.Type.(string); ok {
    // Handle string case
} else if typeArr, ok := schema.Type.([]string); ok {
    // Handle array case
}

Pointer Slices

Problem: Creating value slices instead of pointer slices

// ❌ Wrong - OAS3Document.Servers expects []*parser.Server
servers := []parser.Server{{URL: "http://api.example.com"}}

// ✅ Correct - use pointer slice
servers := []*parser.Server{{URL: "http://api.example.com"}}

Document Mutation

Problem: Modifying source documents unintentionally

// ❌ Wrong - may mutate original
modified := sourceDoc
modified.Info.Title = "New Title"  // Changes sourceDoc too!

// ✅ Correct - deep copy first
modified := sourceDoc.DeepCopy()
modified.Info.Title = "New Title"  // Safe

Hardcoded Strings

Problem: Using string literals instead of constants

// ❌ Wrong
if method == "get" { ... }

// ✅ Correct
if method == httputil.MethodGet { ... }

Missing Tests

Problem: Not testing exported functionality

// ❌ Wrong - exported but no tests
func NewParser() *Parser { ... }

// ✅ Correct - exported with tests
func TestNewParser(t *testing.T) { ... }

License

By contributing to oastools, you agree that your contributions will be licensed under the MIT License.


Happy Contributing! 🎉

We appreciate your time and effort in helping make oastools better. If you have questions or need clarification on anything in this guide, please don't hesitate to ask.