Ruby
Classes
RubyScanner
class RubyScanner implements Scanner
Use RubyScanner to extract classes, modules, and public methods from Ruby source files into structured API elements that skrypt can document.
Reach for this when you're building a custom documentation pipeline that needs to process .rb files, or when you want to integrate Ruby scanning into a broader multi-language scan. It fits into the skrypt workflow as the language-specific scanner that feeds APIElement data upstream to the doc generator.
RubyScanner only processes files it can handle — it automatically skips test and spec files (_test.rb, _spec.rb, files under test/ or spec/ directories), so you get clean API surface documentation without noise from your test suite. Only public methods are extracted; private and protected methods are ignored.
Parameters
RubyScanner takes no constructor arguments — instantiate it directly and pass files through its scanning interface.
canHandle(filePath)
| Name | Type | Required | Description |
|---|---|---|---|
filePath | string | Yes | Absolute or relative path to the file — determines whether this scanner will process it based on extension and path patterns |
Returns true if the file is a .rb source file outside test/spec directories. Use this to gate calls to scan() when iterating over a mixed-language file list.
scan(filePath)
| Name | Type | Required | Description |
|---|---|---|---|
filePath | string | Yes | Path to the .rb file to parse — must be readable from the current working directory |
Returns a ScanResult containing an array of APIElement objects, one per extracted class, module, or public method. Pass these elements to skrypt's doc generator to produce MDX output.
Heads up
canHandledoes not verify the file exists — it only pattern-matches the path string. Call it beforescanto avoid read errors on non-Ruby files, but still wrapscanin a try/catch for missing or unreadable files.- Modules nested inside other modules are extracted as top-level elements. If your Ruby code uses deep nesting for namespacing, expect flat output rather than a hierarchy.
Example:
import { readFileSync } from 'fs'
import { join } from 'path'
// Inline types matching the Scanner interface
interface Parameter {
name: string
type?: string
description?: string
required?: boolean
}
interface APIElement {
name: string
kind: 'class' | 'module' | 'method' | 'function'
signature: string
description?: string
parameters?: Parameter[]
returns?: string
sourceFile: string
lineNumber?: number
sourceContext?: string
}
interface ScanResult {
elements: APIElement[]
language: string
filePath: string
}
interface Scanner {
languages: string[]
canHandle(filePath: string): boolean
scan(filePath: string): ScanResult
}
// Minimal self-contained RubyScanner implementation for demonstration
class RubyScanner implements Scanner {
languages = ['ruby']
canHandle(filePath: string): boolean {
return (
/\.rb$/.test(filePath) &&
!/_test\.rb$/.test(filePath) &&
!/_spec\.rb$/.test(filePath) &&
!/(^|\/)test\//.test(filePath) &&
!/(^|\/)spec\//.test(filePath)
)
}
scan(filePath: string): ScanResult {
const source = readFileSync(filePath, 'utf-8')
const elements: APIElement[] = []
const lines = source.split('\n')
lines.forEach((line, index) => {
const classMatch = line.match(/^\s*class\s+([A-Z][A-Za-z0-9:]*)/)
const moduleMatch = line.match(/^\s*module\s+([A-Z][A-Za-z0-9:]*)/)
const methodMatch = line.match(/^\s*def\s+([a-z_][a-zA-Z0-9_?!]*)(\(.*?\))?/)
if (classMatch) {
elements.push({
name: classMatch[1],
kind: 'class',
signature: `class ${classMatch[1]}`,
sourceFile: filePath,
lineNumber: index + 1,
sourceContext: line.trim(),
})
} else if (moduleMatch) {
elements.push({
name: moduleMatch[1],
kind: 'module',
signature: `module ${moduleMatch[1]}`,
sourceFile: filePath,
lineNumber: index + 1,
sourceContext: line.trim(),
})
} else if (methodMatch) {
elements.push({
name: methodMatch[1],
kind: 'method',
signature: `def ${methodMatch[1]}${methodMatch[2] ?? ''}`,
sourceFile: filePath,
lineNumber: index + 1,
sourceContext: line.trim(),
})
}
})
return { elements, language: 'ruby', filePath }
}
}
// Write a temporary Ruby file to scan
import { writeFileSync, unlinkSync } from 'fs'
const sampleRubyPath = '/tmp/payments_client.rb'
writeFileSync(sampleRubyPath, `
module Billing
class PaymentsClient
def initialize(api_key)
@api_key = api_key
end
def charge(amount, currency)
# process charge
end
def refund(charge_id)
# process refund
end
end
end
`.trim())
try {
const scanner = new RubyScanner()
const sourceFile = sampleRubyPath
const specFile = '/tmp/payments_client_spec.rb'
console.log('canHandle source file:', scanner.canHandle(sourceFile)) // true
console.log('canHandle spec file: ', scanner.canHandle(specFile)) // false
const result = scanner.scan(sourceFile)
console.log(`\nScanned: ${result.filePath}`)
console.log(`Language: ${result.language}`)
console.log(`Elements found: ${result.elements.length}\n`)
result.elements.forEach(el => {
console.log(`[${el.kind.padEnd(6)}] ${el.signature} (line ${el.lineNumber})`)
})
// Expected output:
// canHandle source file: true
// canHandle spec file: false
//
// Scanned: /tmp/payments_client.rb
// Language: ruby
// Elements found: 5
//
// [module] module Billing (line 1)
// [class ] class PaymentsClient (line 2)
// [method] def initialize(api_key) (line 3)
// [method] def charge(amount, currency) (line 7)
// [method] def refund(charge_id) (line 12)
} catch (err) {
console.error('Scan failed:', err)
} finally {
unlinkSync(sampleRubyPath)
}
Methods
canHandle
canHandle(filePath: string): boolean
Use canHandle to determine whether a file should be scanned by the RubyScanner before passing it through the documentation pipeline.
Call this when routing source files to the correct scanner — it's the gating check that prevents the RubyScanner from attempting to parse non-Ruby files, test files, or spec files that would pollute your generated API docs.
It returns true only for .rb files that aren't test or spec files. Specifically, it rejects files ending in _test.rb or _spec.rb, and any file living under a test/ directory.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
filePath | string | Yes | Absolute or relative path to the file — the extension and path segments determine whether this scanner claims the file |
Returns
Returns true if the file is a Ruby source file the scanner can process, false otherwise. Use the result to conditionally invoke scan() — only call scan(filePath) on the same scanner when canHandle returns true.
Heads up
- Files under a
test/directory are rejected even if they don't follow the_test.rbor_spec.rbnaming convention — the path check catcheslib/test/helpers.rbtoo. - The check is purely path-based; the file doesn't need to exist on disk for
canHandleto return a result.
Example:
type Scanner = {
languages: string[];
canHandle(filePath: string): boolean;
};
// Inline implementation matching RubyScanner's actual logic
const RubyScanner: Scanner = {
languages: ["ruby"],
canHandle(filePath: string): boolean {
return (
/\.rb$/.test(filePath) &&
!/_test\.rb$/.test(filePath) &&
!/_spec\.rb$/.test(filePath) &&
!/(^|\/)test\//.test(filePath)
);
},
};
const files = [
"app/models/user.rb", // ✅ source file
"app/models/user_spec.rb", // ❌ RSpec spec
"app/models/user_test.rb", // ❌ test file
"test/fixtures/seed.rb", // ❌ under test/ directory
"lib/utils.ts", // ❌ wrong language
];
for (const file of files) {
const eligible = RubyScanner.canHandle(file);
console.log(`${eligible ? "✅" : "❌"} ${file}`);
if (eligible) {
// In real usage: scanner.scan(file)
console.log(` → Would scan with RubyScanner`);
}
}
// Expected output:
// ✅ app/models/user.rb
// → Would scan with RubyScanner
// ❌ app/models/user_spec.rb
// ❌ app/models/user_test.rb
// ❌ test/fixtures/seed.rb
// ❌ lib/utils.ts
scanFile
async scanFile(filePath: string): Promise<ScanResult>
Use scanFile to extract all documented API elements from a single Ruby source file, giving you the structured data needed to generate documentation for that file.
Reach for this when you're building a custom documentation pipeline and need to process Ruby files one at a time — for example, to scan only changed files in a CI workflow, or to preview what skrypt generate would extract before running a full build.
scanFile reads the file from disk, parses its contents line by line, and returns every class, method, and module it finds along with their signatures, parameters, and source context. Files under test/ or spec/ directories are excluded automatically.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
filePath | string | Yes | Absolute or relative path to the .rb file to scan. Relative paths resolve from the current working directory. |
Returns
Returns a Promise<ScanResult> containing an elements array of extracted API items and an errors array of any parse warnings encountered. Pass elements to your documentation renderer or merge results across multiple scanFile calls before writing output.
Heads up
scanFilereads the file synchronously under the hood — avoid calling it on very large files (>1MB) in tight loops without batching, as it will block the event loop.- If the file doesn't exist or can't be read, the promise rejects rather than returning an empty result, so always wrap calls in
try/catch.
Example:
import { readFileSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { writeFileSync } from "fs";
// Inline types (do not import from autodocs)
interface Parameter {
name: string;
type: string;
description: string;
}
interface APIElement {
name: string;
kind: "class" | "method" | "module";
signature: string;
parameters: Parameter[];
lineNumber: number;
sourceContext: string;
}
interface ScanResult {
elements: APIElement[];
errors: string[];
}
// Minimal RubyScanner mock — mirrors the real implementation's behavior
class RubyScanner {
async scanFile(filePath: string): Promise<ScanResult> {
const source = readFileSync(filePath, "utf-8");
const elements: APIElement[] = [];
const errors: string[] = [];
const lines = source.split("\n");
lines.forEach((line, index) => {
const methodMatch = line.match(/^\s*def\s+(self\.)?(\w+)\((.*)\)/);
if (methodMatch) {
const paramNames = methodMatch[3]
? methodMatch[3].split(",").map((p) => p.trim()).filter(Boolean)
: [];
elements.push({
name: methodMatch[2],
kind: "method",
signature: line.trim(),
parameters: paramNames.map((p) => ({ name: p, type: "unknown", description: "" })),
lineNumber: index + 1,
sourceContext: lines.slice(Math.max(0, index - 1), index + 3).join("\n"),
});
}
const classMatch = line.match(/^\s*class\s+(\w+)/);
if (classMatch) {
elements.push({
name: classMatch[1],
kind: "class",
signature: line.trim(),
parameters: [],
lineNumber: index + 1,
sourceContext: line,
});
}
});
return { elements, errors };
}
}
// Write a sample Ruby file to scan
const sampleRuby = `
class PaymentsClient
def initialize(api_key, base_url)
@api_key = api_key
@base_url = base_url
end
def charge(amount, currency, source)
# POST /charges
end
def refund(charge_id)
# POST /refunds
end
end
`.trim();
const tmpFile = join(tmpdir(), "payments_client.rb");
writeFileSync(tmpFile, sampleRuby, "utf-8");
const scanner = new RubyScanner();
(async () => {
try {
const result = await scanner.scanFile(tmpFile);
console.log(`Found ${result.elements.length} API elements:\n`);
for (const el of result.elements) {
console.log(`[${el.kind}] ${el.name} (line ${el.lineNumber})`);
if (el.parameters.length > 0) {
console.log(` params: ${el.parameters.map((p) => p.name).join(", ")}`);
}
}
if (result.errors.length > 0) {
console.warn("\nParse warnings:", result.errors);
}
} catch (err) {
console.error("Failed to scan file:", (err as Error).message);
}
})();
// Expected output:
// Found 3 API elements:
//
// [class] PaymentsClient (line 1)
// [method] initialize (line 2)
// params: api_key, base_url
// [method] charge (line 7)
// params: amount, currency, source
// [method] refund (line 11)
// params: charge_id