Documentation
¶
Overview ¶
Package customrules provides support for writing OpenAPI linter rules in TypeScript/JavaScript.
This package enables users to extend the OpenAPI linter with custom rules written in TypeScript or JavaScript, without modifying the core linter code. Rules are transpiled using esbuild and executed in a goja JavaScript runtime.
Usage ¶
Import this package for its side effects to enable custom rule loading:
import (
_ "github.com/speakeasy-api/openapi/openapi/linter/customrules"
"github.com/speakeasy-api/openapi/openapi/linter"
)
func main() {
config := &linter.Config{
Extends: []string{"recommended"},
CustomRules: &linter.CustomRulesConfig{
Paths: []string{"./rules/*.ts"},
},
}
lint, err := linter.NewLinter(config)
// ...
}
Writing Custom Rules ¶
Custom rules are written in TypeScript using the @speakeasy-api/openapi-linter-types package for type definitions. Rules can extend the Rule base class or implement the RuleRunner interface.
Example rule (require-summary.ts):
import { Rule, registerRule, createValidationError } from '@speakeasy-api/openapi-linter-types';
import type { Context, DocumentInfo, RuleConfig, ValidationError } from '@speakeasy-api/openapi-linter-types';
class RequireOperationSummary extends Rule {
id(): string { return 'custom-require-operation-summary'; }
category(): string { return 'style'; }
description(): string { return 'All operations must have a summary.'; }
summary(): string { return 'Operations must have summary'; }
run(ctx: Context, docInfo: DocumentInfo, config: RuleConfig): ValidationError[] {
const errors: ValidationError[] = [];
for (const opNode of docInfo.index.operations) {
const op = opNode.node;
if (!op.getSummary()) {
errors.push(createValidationError(
config.getSeverity(this.defaultSeverity()),
this.id(),
'Operation is missing a summary',
op.getRootNode()
));
}
}
return errors;
}
}
registerRule(new RequireOperationSummary());
Configuration ¶
Custom rules are configured in the linter configuration file (lint.yaml):
extends: recommended
custom_rules:
paths:
- ./rules/*.ts
- ./rules/security/*.ts
timeout: 30s # Optional timeout per rule (default: 30s)
rules:
- id: custom-require-operation-summary
severity: error
API Access ¶
Custom rules have access to the full document structure through the DocumentInfo object:
- docInfo.document - The parsed OpenAPI document
- docInfo.location - The file path or URL of the document
- docInfo.index - Pre-computed index with collections for efficient iteration:
- index.operations - All operations in the document
- index.componentSchemas - All component schemas
- index.inlineSchemas - All inline schemas
- And many more collections...
Field and method names are lowercased in JavaScript (e.g., getSummary(), not GetSummary()) due to goja's UncapFieldNameMapper.
Error Handling ¶
Runtime errors in custom rules (exceptions, timeouts) are caught and returned as validation errors. Source maps are used to map error locations back to the original TypeScript source.
Thread Safety ¶
Each rule file gets its own goja runtime instance. Goja runtimes are not thread-safe, so rules should not share state between executions. The Loader maintains a cache of transpiled code but creates new runtimes for each load.
Limitations ¶
- No npm package resolution - rules must be self-contained or use the types package
- No file system access from JavaScript
- No network access from JavaScript
- Memory and CPU limits should be configured via the timeout setting
Index ¶
- Constants
- func ExtractInlineSourceMap(code string) (*sourcemap.Consumer, error)
- func SetDefaultConfig(config *Config)
- type BridgedContext
- type Config
- type CustomRule
- func (r *CustomRule) Category() string
- func (r *CustomRule) DefaultSeverity() validation.Severity
- func (r *CustomRule) Description() string
- func (r *CustomRule) ID() string
- func (r *CustomRule) Link() string
- func (r *CustomRule) Run(ctx context.Context, docInfo *linter.DocumentInfo[*openapi.OpenAPI], ...) []error
- func (r *CustomRule) Summary() string
- func (r *CustomRule) Versions() []string
- type JSFix
- type Loader
- type Logger
- type MappedError
- type Runtime
- func (rt *Runtime) CallMethod(obj goja.Value, method string, args ...goja.Value) (goja.Value, error)
- func (rt *Runtime) ClearInterrupt()
- func (rt *Runtime) ClearRegisteredRules()
- func (rt *Runtime) GetRegisteredRules() []goja.Value
- func (rt *Runtime) Interrupt(reason string)
- func (rt *Runtime) RunScript(name, code string) (goja.Value, error)
- func (rt *Runtime) ToValue(v any) goja.Value
- type TranspiledRule
Constants ¶
const DefaultTimeout = 30 * time.Second
DefaultTimeout is the default execution timeout for custom rules.
Variables ¶
This section is empty.
Functions ¶
func ExtractInlineSourceMap ¶
ExtractInlineSourceMap extracts and parses an inline source map from JavaScript code.
func SetDefaultConfig ¶
func SetDefaultConfig(config *Config)
SetDefaultConfig sets the default config for the loader. This allows customizing the loader with programmatic options (like Logger). Call this before creating any linters to take effect. This function is thread-safe.
Types ¶
type BridgedContext ¶
type BridgedContext struct {
// contains filtered or unexported fields
}
BridgedContext wraps a Go context for JavaScript access.
func NewBridgedContext ¶
func NewBridgedContext(ctx context.Context) *BridgedContext
NewBridgedContext creates a JavaScript-accessible context wrapper.
func (*BridgedContext) Deadline ¶
func (bc *BridgedContext) Deadline() any
Deadline returns the deadline in milliseconds since epoch, or undefined if no deadline.
func (*BridgedContext) IsCancelled ¶
func (bc *BridgedContext) IsCancelled() bool
IsCancelled returns true if the context has been cancelled.
type Config ¶
type Config struct {
// Paths are glob patterns for rule files (e.g., "./rules/*.ts")
Paths []string `yaml:"paths,omitempty" json:"paths,omitempty"`
// Timeout is the maximum execution time per rule (default: 30s)
Timeout time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty"`
// Logger is used for console output from rules (programmatic only, not from YAML)
Logger Logger `yaml:"-" json:"-"`
}
Config configures custom rule loading.
func (*Config) GetTimeout ¶
GetTimeout returns the configured timeout or the default.
type CustomRule ¶
type CustomRule struct {
// contains filtered or unexported fields
}
CustomRule wraps a JavaScript rule object and implements RuleRunner.
func NewCustomRule ¶
func NewCustomRule(rt *Runtime, jsRule goja.Value, sourceFile string, sm *sourcemap.Consumer, config *Config) (*CustomRule, error)
NewCustomRule creates a new CustomRule from a JavaScript rule object.
func (*CustomRule) Category ¶
func (r *CustomRule) Category() string
func (*CustomRule) DefaultSeverity ¶
func (r *CustomRule) DefaultSeverity() validation.Severity
func (*CustomRule) Description ¶
func (r *CustomRule) Description() string
func (*CustomRule) ID ¶
func (r *CustomRule) ID() string
func (*CustomRule) Link ¶
func (r *CustomRule) Link() string
func (*CustomRule) Run ¶
func (r *CustomRule) Run(ctx context.Context, docInfo *linter.DocumentInfo[*openapi.OpenAPI], config *linter.RuleConfig) []error
Run executes the JavaScript rule against the document.
func (*CustomRule) Summary ¶
func (r *CustomRule) Summary() string
func (*CustomRule) Versions ¶
func (r *CustomRule) Versions() []string
type JSFix ¶
type JSFix struct {
// contains filtered or unexported fields
}
JSFix bridges a JavaScript fix object to the Go Fix interface.
func (*JSFix) Description ¶
func (*JSFix) Interactive ¶
func (*JSFix) Prompts ¶
func (f *JSFix) Prompts() []validation.Prompt
type Loader ¶
type Loader struct {
// contains filtered or unexported fields
}
Loader loads custom rules from TypeScript/JavaScript files.
func (*Loader) LoadRules ¶
func (l *Loader) LoadRules(baseConfig *baseLinter.CustomRulesConfig) ([]baseLinter.RuleRunner[*openapi.OpenAPI], error)
LoadRules loads all custom rules from the configured paths.
type MappedError ¶
MappedError wraps an error with original source location.
func MapException ¶
MapException maps a goja exception to original TypeScript source locations.
func (*MappedError) Error ¶
func (e *MappedError) Error() string
func (*MappedError) Unwrap ¶
func (e *MappedError) Unwrap() error
type Runtime ¶
type Runtime struct {
// contains filtered or unexported fields
}
Runtime wraps a goja JavaScript runtime with custom rule support. Each Runtime instance is NOT thread-safe and should only be used from a single goroutine.
func NewRuntime ¶
NewRuntime creates a new JavaScript runtime configured for custom rules.
func (*Runtime) CallMethod ¶
func (rt *Runtime) CallMethod(obj goja.Value, method string, args ...goja.Value) (goja.Value, error)
CallMethod calls a method on a JavaScript object.
func (*Runtime) ClearInterrupt ¶
func (rt *Runtime) ClearInterrupt()
ClearInterrupt clears any pending interrupt.
func (*Runtime) ClearRegisteredRules ¶
func (rt *Runtime) ClearRegisteredRules()
ClearRegisteredRules clears the registered rules list.
func (*Runtime) GetRegisteredRules ¶
GetRegisteredRules returns all rules registered via registerRule().
func (*Runtime) Interrupt ¶
Interrupt interrupts the currently running JavaScript. This is used for timeout handling.