fix(ruby): accept acronyms in module name validation
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 appliescaser().ToPascal. It keeps acronyms inside mixed-case names (MySDK,OpenAPI,HTTPClientare unchanged) and always keepsSDK. A name that is only an uppercase run other thanSDK, 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
OpenApiSdkalready renders asmodule OpenApiSDKin 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\u002fescape that goja'sRegExp.sourceemits for an unescaped/and compiles the result with Go'sregexp.MustCompile. A JavaScript-only construct such as lookahead, lookbehind, or a backreference is valid inconfig.tsbut panics the CLI for every user of that target. - A
DefaultValuethat 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:modulepattern^([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:SDKGenConfigFieldgains an optionalValidationExamples: { valid?: string[]; invalid?: string[] }. It is test-only:GetLanguageConfigFieldsdecodes into sdk-gen-config'sSDKGenConfigField, which drops unknown keys, so the CLI,speakeasy configure, andcmd/jsonschemanever see it and sdk-gen-config needs no change. The templates'tsconfig.jsonfiles do not includeconfig.ts, so the type documents the key but does not check it; the test below does.templates/templates/ruby/config.ts: themodulefield declares its accepted (AcmeRubySDK,OpenAPI,SDK,Acme2, and the single letterA) and rejected (openApiSdk,Open_Api,Open::Api,Open-Api,2Acme, empty) names asValidationExamples.pkg/templates/validation_regex_test.go:TestValidationRegexesruns acommonsubtest overcommon/common/config.tsand one subtest per template fromGetAvailableTemplates(), each withnewSDKbothfalseandtrue. It reads the rawconfig.tsoutput (soValidationExamplessurvive) and decodes each field intoSDKGenConfigFieldplusValidationExampleswith mapstructure'sErrorUnused, so a misspelled key fails instead of being silently dropped.ValidationFuncis also accepted, because two existing fields (mcp-typescriptadvancedFlags,typescriptv2usageSDKInitImports) declare it even though neither the generator nor the CLI reads it. It then compiles everyValidationRegexafter applying the CLI's slash rewrite, and asserts that theDefaultValueand everyvalidexample match and everyinvalidexample does not. A field that declares examples without aValidationRegexfails, so removing a pattern cannot silently skip its examples. Common fields that a target passes through unchanged are checked once in thecommonsubtest. Each template subtest also assertsGetLanguageConfigFieldsstill 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/rubypatch.
Generated output does not change, so no review SDKs are regenerated.
Testing
- Generated a Ruby SDK with
module: AcmeRubySDKandtypingStrategy: sorbetviacmd/generate: emitsmodule AcmeRubySDKunderlib/acme_ruby_sdk/. go test ./pkg/templates -run TestValidationRegexes -count=1 -race: passes. Mutation checks, each reverted afterwards:- lookahead in the Ruby
modulepattern (^(?=O)[A-Za-z]+$): fails with a compile error naming the field; - Ruby
moduledefaultopenApiSdk: fails naming the field, default, and pattern; - previous Ruby
modulepattern^([A-Z][a-z0-9]+)+$: fails on valid examplesAcmeRubySDK,OpenAPI,SDK; Open_Apiadded tovalid,Acmeadded toinvalid: each fails naming the example;- a
validexamplemaybeon a common^(true|false)$field: fails in thecommonsubtest; - key misspelled as
ValidationExample, orvalidmisspelled inside it: each fails with an unknown-key error naming the field; - Ruby
modulepattern removed while its examples stay: fails naming the field; *changed to+in the Rubymodulepattern: fails on the valid exampleA;- removing the slash rewrite: fails on the
clitarget'spackageName, whose pattern contains/.
- lookahead in the Ruby
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.