fix(ruby): accept acronyms in module name validation

2Y
14h ago
+150-1

Why

The Ruby module gen.yaml field is validated against ValidationRegex in templates/templates/ruby/config.ts. Its ValidationMessage reads "Module names should be PascalCase and have no special chars", but the current pattern ^([A-Z][a-z0-9]+)+$ is stricter than that message: every capital must be followed by a lowercase letter or digit, so acronyms are rejected. MySDK, OpenAPI, and SDK all fail.

PascalCase allows acronyms; Ruby's own standard library uses them (JSON, URI, OpenSSL, Net::HTTP). The generator already handles them as well:

  • Every template reference to the module goes through sanitizeModuleName (templates/templates/ruby/includes/sanitization.ts), which applies caser().ToPascal. It keeps acronyms inside mixed-case names (MySDK, OpenAPI, HTTPClient are unchanged) and always keeps SDK. A name that is only an uppercase run other than SDK, or that ends in an acronym followed by a digit, is rewritten (API → Api, OpenAPIV3 → OpenApiv3); the result is still a valid Ruby constant.
  • File paths go through caser().ToSnake (getSDKFolderName, templateAuxPath): MySDK → lib/my_sdk/.
  • The default value OpenApiSdk already renders as module OpenApiSDK in the review and primary Ruby SDKs, so acronym constants are already normal output.

The pattern is the only thing blocking these names. The Speakeasy CLI enforces it during speakeasy run, so an SDK whose module contains an acronym fails gen.yaml validation on every generator release that includes the current pattern. When speakeasyVersion is latest, the CLI then reruns with the last version that succeeded, so the SDK quietly stops receiving generator updates and the run can still report success.

No coverage for ValidationRegex in any target

Nothing in this repository exercises ValidationRegex. The generator never reads it; only cmd/jsonschema copies it into a JSON Schema pattern. Generation and SDK tests therefore pass whatever the pattern says, which is how the Ruby regression went unnoticed. The same gap exists for every target:

  • Patterns are written as JavaScript regular expressions and exported through RegExp.source, but the CLI rewrites the \u002f escape that goja's RegExp.source emits for an unescaped / and compiles the result with Go's regexp.MustCompile. A JavaScript-only construct such as lookahead, lookbehind, or a backreference is valid in config.ts but panics the CLI for every user of that target.
  • A DefaultValue that fails its own pattern makes a freshly initialised gen.yaml fail validation.

Today all 241 patterns across the available templates (3 common, 238 target-specific, the same count for newSDK false and true) compile in Go and accept their defaults, so the check below is a guard against regressions.

What changed

  • templates/templates/ruby/config.ts: module pattern ^([A-Z][a-z0-9]+)+$ → ^[A-Z][A-Za-z0-9]*$. This is exactly what the existing message says: an uppercase first letter, then letters and digits only. Names that passed before still pass; _, ::, -, a lowercase first letter, and a leading digit are still rejected.
  • templates/templates/common/common/typeDefs.d.ts: SDKGenConfigField gains an optional ValidationExamples: { valid?: string[]; invalid?: string[] }. It is test-only: GetLanguageConfigFields decodes into sdk-gen-config's SDKGenConfigField, which drops unknown keys, so the CLI, speakeasy configure, and cmd/jsonschema never see it and sdk-gen-config needs no change. The templates' tsconfig.json files do not include config.ts, so the type documents the key but does not check it; the test below does.
  • templates/templates/ruby/config.ts: the module field declares its accepted (AcmeRubySDK, OpenAPI, SDK, Acme2, and the single letter A) and rejected (openApiSdk, Open_Api, Open::Api, Open-Api, 2Acme, empty) names as ValidationExamples.
  • pkg/templates/validation_regex_test.go: TestValidationRegexes runs a common subtest over common/common/config.ts and one subtest per template from GetAvailableTemplates(), each with newSDK both false and true. It reads the raw config.ts output (so ValidationExamples survive) and decodes each field into SDKGenConfigField plus ValidationExamples with mapstructure's ErrorUnused, so a misspelled key fails instead of being silently dropped. ValidationFunc is also accepted, because two existing fields (mcp-typescript advancedFlags, typescriptv2 usageSDKInitImports) declare it even though neither the generator nor the CLI reads it. It then compiles every ValidationRegex after applying the CLI's slash rewrite, and asserts that the DefaultValue and every valid example match and every invalid example does not. A field that declares examples without a ValidationRegex fails, so removing a pattern cannot silently skip its examples. Common fields that a target passes through unchanged are checked once in the common subtest. Each template subtest also asserts GetLanguageConfigFields still succeeds. A default-value check alone cannot catch a pattern that is too strict while the default still passes; examples next to the pattern cover that without a test file per target.
  • Changeset: core / ruby patch.

Generated output does not change, so no review SDKs are regenerated.

Testing

  • Generated a Ruby SDK with module: AcmeRubySDK and typingStrategy: sorbet via cmd/generate: emits module AcmeRubySDK under lib/acme_ruby_sdk/.
  • go test ./pkg/templates -run TestValidationRegexes -count=1 -race: passes. Mutation checks, each reverted afterwards:
    • lookahead in the Ruby module pattern (^(?=O)[A-Za-z]+$): fails with a compile error naming the field;
    • Ruby module default openApiSdk: fails naming the field, default, and pattern;
    • previous Ruby module pattern ^([A-Z][a-z0-9]+)+$: fails on valid examples AcmeRubySDK, OpenAPI, SDK;
    • Open_Api added to valid, Acme added to invalid: each fails naming the example;
    • a valid example maybe on a common ^(true|false)$ field: fails in the common subtest;
    • key misspelled as ValidationExample, or valid misspelled inside it: each fails with an unknown-key error naming the field;
    • Ruby module pattern removed while its examples stay: fails naming the field;
    • * changed to + in the Ruby module pattern: fails on the valid example A;
    • removing the slash rewrite: fails on the cli target's packageName, whose pattern contains /.
  • make lint: passes.
  • Not run: end-to-end speakeasy run, which needs a CLI build bundling this generator.

Public-safety check

  • This change contains no credentials, customer documents, private repository URLs, private filesystem paths, or unredacted private logs.
  • Title, body, comments, and commit messages name no customers or customer-derived identifiers, private paths or trackers, or workflow provenance, and are understandable without private context (.claude/skills/public-repo-communication/SKILL.md).
  • Generated fixtures and review SDK changes are public-safe.
  • I reviewed git diff --check.