Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changesets/1793042947-3a9d1266.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
id: 1793042947-3a9d1266
features:
- core
targets:
- go
type: fix
bump: patch
description: accept retractions lists in gen.yaml validation
author: 2ynn
date: "2026-10-26"
10 changes: 10 additions & 0 deletions .changesets/1793042976-c1371213.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
id: 1793042976-c1371213
features:
- core
targets:
- mcp-typescript
type: fix
bump: patch
description: accept every MCP registry server name in mcpName
author: 2ynn
date: "2026-10-26"
10 changes: 10 additions & 0 deletions .changesets/1793045736-0d9318ab.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
id: 1793045736-0d9318ab
features:
- core
targets:
- php
type: fix
bump: patch
description: reject Composer package names with trailing invalid characters
author: 2ynn
date: "2026-10-26"
10 changes: 10 additions & 0 deletions .changesets/1793046178-b45e569c.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
id: 1793046178-b45e569c
features:
- core
targets:
- javav2
type: fix
bump: patch
description: write correct Maven POM links when githubURL includes a scheme, www., trailing slash or .git
author: 2ynn
date: "2026-10-26"
103 changes: 103 additions & 0 deletions pkg/generate/java_publishing_links_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package generate

import (
"context"
"os"
"path/filepath"
"testing"

generationaccess "github.com/speakeasy-api/generation-context/access"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

const javaPublishingLinksSpec = `openapi: 3.1.0
info:
title: Acme
version: 1.0.0
servers:
- url: https://api.example.com
paths:
/widgets:
get:
operationId: listWidgets
responses:
"200":
description: OK
`

func TestJavaPublishingLinksNormalizeGithubURL(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
githubURL string
pomURL string
scmURL string
scmConnection string
}{
"canonical": {
githubURL: "github.com/acme/acme-java",
pomURL: "https://github.com/acme/acme-java",
scmURL: "github.com/acme/acme-java",
scmConnection: "scm:git:ssh://git@github.com/acme/acme-java.git",
},
"scheme, www, .git and trailing slash": {
githubURL: "https://www.github.com/acme/acme-java.git/",
pomURL: "https://github.com/acme/acme-java",
scmURL: "github.com/acme/acme-java",
scmConnection: "scm:git:ssh://git@github.com/acme/acme-java.git",
},
"uppercase scheme, www and .git": {
githubURL: "HTTPS://WWW.github.com/acme/acme-java.GIT",
pomURL: "https://github.com/acme/acme-java",
scmURL: "github.com/acme/acme-java",
scmConnection: "scm:git:ssh://git@github.com/acme/acme-java.git",
},
"non-string value": {
githubURL: "123",
pomURL: "https://123",
scmURL: "123",
scmConnection: "scm:git:ssh://git@123.git",
},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()

outDir := t.TempDir()
speakeasyDir := filepath.Join(outDir, ".speakeasy")
require.NoError(t, os.MkdirAll(speakeasyDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(speakeasyDir, "gen.yaml"), []byte(`configVersion: 2.0.0
generation:
sdkClassName: Acme
java:
version: 0.0.1
groupID: com.acme
artifactID: acme
githubURL: `+tc.githubURL+`
`), 0o644))

g, err := New(WithPublished(true))
require.NoError(t, err)

errs := g.Generate(
generationaccess.WithDirect(context.Background(), generationaccess.ElectAGPL()),
[]byte(javaPublishingLinksSpec),
"openapi.yaml",
"java",
outDir,
false,
false,
)
require.Empty(t, errs)

publishing, err := os.ReadFile(filepath.Join(outDir, "publishing.gradle"))
require.NoError(t, err)

assert.Contains(t, string(publishing), " url = '"+tc.pomURL+"'\n")
assert.Contains(t, string(publishing), " url = '"+tc.scmURL+"'\n")
assert.Contains(t, string(publishing), " connection = '"+tc.scmConnection+"'\n")
})
}
}
24 changes: 24 additions & 0 deletions pkg/templates/validation_regex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package templates
import (
"fmt"
"regexp"
"slices"
"strings"
"testing"

Expand All @@ -27,6 +28,29 @@ type validationField struct {
ValidationFunc any
}

// The CLI matches ValidationRegex against the value formatted with %v, so a pattern on a list field rejects every list.
// List fields with a default are already caught by the default value check.
var listFieldsWithoutDefault = map[string][]string{
"go": {"retractions"},
}

func TestListFieldsHaveNoValidationRegex(t *testing.T) {
t.Parallel()

for template, names := range listFieldsWithoutDefault {
for _, newSDK := range []bool{false, true} {
fields, err := GetLanguageConfigFields(types.NewTargetFromTemplate(template), newSDK)
require.NoError(t, err)

for _, name := range names {
i := slices.IndexFunc(fields, func(field config.SDKGenConfigField) bool { return field.Name == name })
require.NotEqual(t, -1, i, "%s field %q not found (newSDK=%v)", template, name, newSDK)
assert.Nil(t, fields[i].ValidationRegex, "%s field %q (newSDK=%v)", template, name, newSDK)
}
}
}
}

func TestValidationRegexes(t *testing.T) {
t.Parallel()

Expand Down
24 changes: 24 additions & 0 deletions templates/templates/cli/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,18 @@ function getConfigFields(
"The go module package name. https://go.dev/ref/mod#module-path.",
ValidationRegex: /^[\w\d.\-_/]+$/.source,
ValidationMessage: "Letters, numbers, or /.-_ only",
ValidationExamples: {
valid: [
"acme-cli",
"github.com/acme/acme-cli",
"example.com/acme/cli/v2",
],
invalid: [
"",
"github.com/acme/acme cli",
"https://github.com/acme/acme-cli",
],
},
},
cliName: {
Name: "cliName",
Expand All @@ -326,6 +338,10 @@ function getConfigFields(
ValidationRegex: /^[a-z][a-z0-9\-]*$/.source,
ValidationMessage:
"Lowercase letters, numbers, and hyphens only. Must start with a letter.",
ValidationExamples: {
valid: ["acme", "acme-cli", "acme2"],
invalid: ["", "Acme", "acme_cli", "2acme", "-acme"],
},
},
envVarPrefix: {
Name: "envVarPrefix",
Expand All @@ -336,6 +352,10 @@ function getConfigFields(
ValidationRegex: /^[A-Z][A-Z0-9_]*$/.source,
ValidationMessage:
"Uppercase letters, numbers, and underscores only. Must start with a letter.",
ValidationExamples: {
valid: ["ACME", "ACME_CLI", "ACME2"],
invalid: ["", "acme", "ACME-CLI", "_ACME", "2ACME"],
},
},
additionalDependencies: {
Name: "additionalDependencies",
Expand Down Expand Up @@ -482,6 +502,10 @@ function getConfigFields(
"Default network timeout applied to each API operation when --timeout is not given (a Go duration such as 60s). Bounds one HTTP operation including retries; operations whose body outlives the call (event streams, JSONL, raw response streams) are exempt so long-lived reads survive, and polling loops re-arm it per request without capping their own total budget. Empty or zero disables the default.",
ValidationRegex: /^$|^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$/.source,
ValidationMessage: "a Go duration such as 60s or 2m, or empty",
ValidationExamples: {
valid: ["", "60s", "2m", "1h30m", "1.5s", "500ms", "250µs"],
invalid: ["60", "1d", "-5s", "60 s", "s"],
},
},
jqRawOutput: {
Name: "jqRawOutput",
Expand Down
8 changes: 8 additions & 0 deletions templates/templates/common/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ function getCommonConfigFields(newSDK: boolean): SDKGenConfigFields {
"The suffix to add to models with writeOnly fields that are created as input models",
ValidationRegex: /^[\w\d.\-_]*$/.source,
ValidationMessage: "Letters, numbers, or .-_ only",
ValidationExamples: {
valid: ["input", "Input", "request_input", ""],
invalid: ["input v2", "input/v2", "input!"],
},
},
outputModelSuffix: {
Name: "outputModelSuffix",
Expand All @@ -19,6 +23,10 @@ function getCommonConfigFields(newSDK: boolean): SDKGenConfigFields {
"The suffix to add to models with writeOnly fields that are created as input models",
ValidationRegex: /^[\w\d.\-_]*$/.source,
ValidationMessage: "Letters, numbers, or .-_ only",
ValidationExamples: {
valid: ["output", "Output", "request_output", ""],
invalid: ["output v2", "output/v2", "output!"],
},
},
customCasings: {
Name: "customCasings",
Expand Down
24 changes: 24 additions & 0 deletions templates/templates/csharp/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ function getConfigFields(
Description: "The current version of the SDK",
ValidationRegex: /^[\w\d.\-_]+$/.source,
ValidationMessage: "Letters, numbers, or .-_ only",
ValidationExamples: {
valid: ["0.0.1", "1.2.3", "1.2.3-beta.1"],
invalid: ["", "1.2.3 beta", "1.2.3/beta"],
},
},
description: {
Name: "description",
Expand All @@ -199,6 +203,10 @@ function getConfigFields(
"The NuGet package ID, also used as the root namespace. https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/names-of-namespaces.",
ValidationRegex: /^[\w\d._]+$/.source,
ValidationMessage: "Letters, numbers, or ._ only",
ValidationExamples: {
valid: ["Acme", "Acme.Sdk", "Acme.Petstore.Client", "Acme_Sdk"],
invalid: ["", "Acme-Sdk", "Acme Sdk", "Acme/Sdk"],
},
},
sourceDirectory: {
Name: "sourceDirectory",
Expand All @@ -207,6 +215,10 @@ function getConfigFields(
Description: 'The name of the source directory. Default is "src"',
ValidationRegex: /^\w*$/.source,
ValidationMessage: "Letters, numbers and underscores only",
ValidationExamples: {
valid: ["", "src", "Source", "src_v2"],
invalid: ["src/Acme", "src-v2", "./src"],
},
},
disableNamespacePascalCasingApr2024: {
Name: "disableNamespacePascalCasingApr2024",
Expand All @@ -225,6 +237,10 @@ function getConfigFields(
"The maximum number of parameters a method can have before the resulting SDK endpoint is no longer 'flattened' and an input object is created instead. 0 will use input objects always. https://www.speakeasy.com/docs/customize-sdks/methods",
ValidationRegex: /^\d+$/.source,
ValidationMessage: "Numbers only",
ValidationExamples: {
valid: ["0", "4", "999"],
invalid: ["", "-1", "4.5", "four"],
},
},
author: {
Name: "author",
Expand Down Expand Up @@ -408,6 +424,10 @@ function getConfigFields(
"The name of the default exception that is thrown when an API error occurs.",
ValidationRegex: /^[a-zA-Z0-9]*$/.source,
ValidationMessage: "Must only contain letters and numbers",
ValidationExamples: {
valid: ["APIError", "SDKException", "AcmeError2", ""],
invalid: ["Acme_Error", "Acme-Error", "Acme Error", "Acme.Error"],
},
},
httpClientPrefix: {
Name: "httpClientPrefix",
Expand All @@ -417,6 +437,10 @@ function getConfigFields(
"The prefix used to name the HTTP client interface: 'I<Prefix>HttpClient'. If left empty, the SDK name will be used instead.",
ValidationRegex: /^[a-zA-Z0-9]*$/.source,
ValidationMessage: "Must only contain letters and numbers",
ValidationExamples: {
valid: ["", "Acme", "AcmeHttp"],
invalid: ["Acme_", "Acme-Http", "Acme Http"],
},
},
enableCancellationToken: {
Name: "enableCancellationToken",
Expand Down
Loading
Loading