Skip to content

Commit 8b65e0b

Browse files
committed
feat: custom code
1 parent 1466556 commit 8b65e0b

File tree

13 files changed

+1058
-321
lines changed

13 files changed

+1058
-321
lines changed

configuration.go

Lines changed: 78 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
"github.com/mitchellh/mapstructure"
88
"github.com/speakeasy-api/openapi/pointer"
9+
jsg "github.com/swaggest/jsonschema-go"
910
)
1011

1112
const (
@@ -43,21 +44,35 @@ const (
4344
SDKInitStyleBuilder SDKInitStyle = "builder"
4445
)
4546

47+
// ServerIndex is a type that can be either a string (server ID) or an integer (server index)
48+
type ServerIndex string
49+
50+
func (ServerIndex) PrepareJSONSchema(schema *jsg.Schema) error {
51+
// Set the type to an array of types [string, integer]
52+
schema.Type = &jsg.Type{
53+
SliceOfSimpleTypeValues: []jsg.SimpleType{jsg.String, jsg.Integer},
54+
}
55+
schema.WithDescription("Controls which server is shown in usage snippets. If unset, no server will be shown. If an integer, it will be used as the server index. Otherwise, it will look for a matching server ID.")
56+
return nil
57+
}
58+
4659
type UsageSnippets struct {
47-
OptionalPropertyRendering OptionalPropertyRenderingOption `yaml:"optionalPropertyRendering"`
48-
SDKInitStyle SDKInitStyle `yaml:"sdkInitStyle"`
49-
ServerToShowInSnippets string `yaml:"serverToShowInSnippets,omitempty"` // If unset, no server will be shown, if an integer, use as server_idx, else look for a matching id
50-
AdditionalProperties map[string]any `yaml:",inline"` // Captures any additional properties that are not explicitly defined for backwards/forwards compatibility
60+
_ struct{} `additionalProperties:"true" description:"Configuration for usage snippets"`
61+
OptionalPropertyRendering OptionalPropertyRenderingOption `yaml:"optionalPropertyRendering" enum:"always,never,withExample" description:"Controls how optional properties are rendered in usage snippets"`
62+
SDKInitStyle SDKInitStyle `yaml:"sdkInitStyle" enum:"constructor,builder" description:"Controls how the SDK initialization is depicted in usage snippets"`
63+
ServerToShowInSnippets ServerIndex `yaml:"serverToShowInSnippets,omitempty"` // If unset, no server will be shown, if an integer, use as server_idx, else look for a matching id
64+
AdditionalProperties map[string]any `yaml:",inline" jsonschema:"-"` // Captures any additional properties that are not explicitly defined for backwards/forwards compatibility
5165
}
5266

5367
type Fixes struct {
54-
NameResolutionDec2023 bool `yaml:"nameResolutionDec2023,omitempty"`
55-
NameResolutionFeb2025 bool `yaml:"nameResolutionFeb2025"`
56-
ParameterOrderingFeb2024 bool `yaml:"parameterOrderingFeb2024"`
57-
RequestResponseComponentNamesFeb2024 bool `yaml:"requestResponseComponentNamesFeb2024"`
58-
SecurityFeb2025 bool `yaml:"securityFeb2025"`
59-
SharedErrorComponentsApr2025 bool `yaml:"sharedErrorComponentsApr2025"`
60-
AdditionalProperties map[string]any `yaml:",inline"` // Captures any additional properties that are not explicitly defined for backwards/forwards compatibility
68+
_ struct{} `additionalProperties:"true" description:"Fixes applied to the SDK generation"`
69+
NameResolutionDec2023 bool `yaml:"nameResolutionDec2023,omitempty" description:"Enables name resolution fixes from December 2023"`
70+
NameResolutionFeb2025 bool `yaml:"nameResolutionFeb2025" description:"Enables name resolution fixes from February 2025"`
71+
ParameterOrderingFeb2024 bool `yaml:"parameterOrderingFeb2024" description:"Enables parameter ordering fixes from February 2024"`
72+
RequestResponseComponentNamesFeb2024 bool `yaml:"requestResponseComponentNamesFeb2024" description:"Enables request and response component naming fixes from February 2024"`
73+
SecurityFeb2025 bool `yaml:"securityFeb2025" description:"Enables fixes and refactoring for security that were introduced in February 2025"`
74+
SharedErrorComponentsApr2025 bool `yaml:"sharedErrorComponentsApr2025" description:"Enables fixes that mean that when a component is used in both 2XX and 4XX responses, only the top level component will be duplicated to the errors scope as opposed to the entire component tree"`
75+
AdditionalProperties map[string]any `yaml:",inline" jsonschema:"-"` // Captures any additional properties that are not explicitly defined for backwards/forwards compatibility
6176
}
6277

6378
func (f *Fixes) UnmarshalYAML(unmarshal func(interface{}) error) error {
@@ -78,15 +93,32 @@ func (f *Fixes) UnmarshalYAML(unmarshal func(interface{}) error) error {
7893
}
7994

8095
type Auth struct {
81-
OAuth2ClientCredentialsEnabled bool `yaml:"oAuth2ClientCredentialsEnabled"`
82-
OAuth2PasswordEnabled bool `yaml:"oAuth2PasswordEnabled"`
83-
HoistGlobalSecurity bool `yaml:"hoistGlobalSecurity"`
96+
_ struct{} `additionalProperties:"false" description:"Authentication configuration"`
97+
OAuth2ClientCredentialsEnabled bool `yaml:"oAuth2ClientCredentialsEnabled" description:"Enables support for OAuth2 client credentials grant type"`
98+
OAuth2PasswordEnabled bool `yaml:"oAuth2PasswordEnabled" description:"Enables support for OAuth2 resource owner password credentials grant type"`
99+
HoistGlobalSecurity bool `yaml:"hoistGlobalSecurity" description:"Enables hoisting of operation-level security schemes to global level when no global security is defined"`
84100
}
85101

86102
type Tests struct {
87-
GenerateTests bool `yaml:"generateTests"`
88-
GenerateNewTests bool `yaml:"generateNewTests"`
89-
SkipResponseBodyAssertions bool `yaml:"skipResponseBodyAssertions"`
103+
_ struct{} `additionalProperties:"true" description:"Test generation configuration"`
104+
GenerateTests bool `yaml:"generateTests" description:"Enables generation of tests"`
105+
GenerateNewTests bool `yaml:"generateNewTests" description:"Enables generation of new tests for any new operations in the OpenAPI specification"`
106+
SkipResponseBodyAssertions bool `yaml:"skipResponseBodyAssertions" description:"Skip asserting that the client got the same response bodies returned by the mock server"`
107+
AdditionalProperties map[string]any `yaml:",inline" jsonschema:"-"` // Captures any additional properties that are not explicitly defined for backwards/forwards compatibility
108+
}
109+
110+
// PersistentEdits configures whether user edits to generated SDKs persist across regenerations
111+
// When enabled, user changes are preserved via 3-way merge with Git tracking
112+
type PersistentEdits struct {
113+
_ struct{} `additionalProperties:"true" description:"Configures whether user edits to generated SDKs persist across regenerations"`
114+
// Enabled allows user edits to generated SDK code to persist through regeneration
115+
// Requires Git repository and creates a pristine branch for tracking
116+
Enabled bool `yaml:"enabled,omitempty" description:"Enables preservation of user edits across SDK regenerations. Requires Git repository."`
117+
118+
// PristineBranch specifies the Git branch name for tracking pristine generated code
119+
// Defaults to "sdk-pristine" if not specified
120+
PristineBranch string `yaml:"pristineBranch,omitempty" description:"The Git branch name for tracking pristine generated code. Defaults to 'sdk-pristine' if not specified."`
121+
AdditionalProperties map[string]any `yaml:",inline" jsonschema:"-"` // Captures any additional properties
90122
}
91123

92124
type AllOfMergeStrategy string
@@ -97,49 +129,56 @@ const (
97129
)
98130

99131
type Schemas struct {
100-
AllOfMergeStrategy AllOfMergeStrategy `yaml:"allOfMergeStrategy"`
132+
_ struct{} `additionalProperties:"false" description:"Schema processing configuration"`
133+
AllOfMergeStrategy AllOfMergeStrategy `yaml:"allOfMergeStrategy" enum:"deepMerge,shallowMerge" description:"Controls how allOf schemas are merged"`
101134
}
102135

103136
type Generation struct {
137+
_ struct{} `additionalProperties:"true" description:"Generation configuration"`
104138
DevContainers *DevContainers `yaml:"devContainers,omitempty"`
105-
BaseServerURL string `yaml:"baseServerUrl,omitempty"`
106-
SDKClassName string `yaml:"sdkClassName,omitempty"`
107-
MaintainOpenAPIOrder bool `yaml:"maintainOpenAPIOrder,omitempty"`
108-
DeduplicateErrors bool `yaml:"deduplicateErrors,omitempty"`
139+
BaseServerURL string `yaml:"baseServerUrl,omitempty" description:"The base URL of the server. This value will be used if global servers are not defined in the spec."`
140+
SDKClassName string `yaml:"sdkClassName,omitempty" description:"Generated name of the root SDK class"`
141+
MaintainOpenAPIOrder bool `yaml:"maintainOpenAPIOrder,omitempty" description:"Maintains the order of parameters and fields in the OpenAPI specification"`
142+
DeduplicateErrors bool `yaml:"deduplicateErrors,omitempty" description:"Deduplicates errors that have the same schema"`
109143
UsageSnippets *UsageSnippets `yaml:"usageSnippets,omitempty"`
110-
UseClassNamesForArrayFields bool `yaml:"useClassNamesForArrayFields,omitempty"`
144+
UseClassNamesForArrayFields bool `yaml:"useClassNamesForArrayFields,omitempty" description:"Use class names for array fields instead of the child's schema type"`
111145
Fixes *Fixes `yaml:"fixes,omitempty"`
112146
Auth *Auth `yaml:"auth,omitempty"`
113-
SkipErrorSuffix bool `yaml:"skipErrorSuffix,omitempty"`
114-
InferSSEOverload bool `yaml:"inferSSEOverload,omitempty"`
115-
SDKHooksConfigAccess bool `yaml:"sdkHooksConfigAccess,omitempty"`
147+
SkipErrorSuffix bool `yaml:"skipErrorSuffix,omitempty" description:"Skips the automatic addition of an error suffix to error types"`
148+
InferSSEOverload bool `yaml:"inferSSEOverload,omitempty" description:"Generates an overload if generator detects that the request body field 'stream: true' is used for client intent to request 'text/event-stream' response"`
149+
SDKHooksConfigAccess bool `yaml:"sdkHooksConfigAccess,omitempty" description:"Enables access to the SDK configuration from hooks"`
116150
Schemas Schemas `yaml:"schemas"`
117-
RequestBodyFieldName string `yaml:"requestBodyFieldName"`
151+
RequestBodyFieldName string `yaml:"requestBodyFieldName" description:"The name of the field to use for the request body in generated SDKs"`
118152

119153
// Mock server generation configuration.
120154
MockServer *MockServer `yaml:"mockServer,omitempty"`
121155

122-
Tests Tests `yaml:"tests,omitempty"`
156+
// PersistentEdits configures whether user edits persist across regenerations
157+
PersistentEdits *PersistentEdits `yaml:"persistentEdits,omitempty"`
158+
Tests Tests `yaml:"tests,omitempty"`
123159

124-
AdditionalProperties map[string]any `yaml:",inline"` // Captures any additional properties that are not explicitly defined for backwards/forwards compatibility
160+
AdditionalProperties map[string]any `yaml:",inline" jsonschema:"-"` // Captures any additional properties that are not explicitly defined for backwards/forwards compatibility
125161
}
126162

127163
type DevContainers struct {
128-
Enabled bool `yaml:"enabled"`
164+
_ struct{} `additionalProperties:"true" description:"Dev container configuration"`
165+
Enabled bool `yaml:"enabled" description:"Whether dev containers are enabled"`
129166
// This can be a local path or a remote URL
130-
SchemaPath string `yaml:"schemaPath"`
131-
AdditionalProperties map[string]any `yaml:",inline"` // Captures any additional properties that are not explicitly defined for backwards/forwards compatibility
167+
SchemaPath string `yaml:"schemaPath" description:"Path to the schema file for the dev container"`
168+
AdditionalProperties map[string]any `yaml:",inline" jsonschema:"-"` // Captures any additional properties that are not explicitly defined for backwards/forwards compatibility
132169
}
133170

134171
// Generation configuration for the inter-templated mockserver target for test generation.
135172
type MockServer struct {
173+
_ struct{} `additionalProperties:"false" description:"Mock server generation configuration"`
136174
// Disables the code generation of the mockserver target.
137-
Disabled bool `yaml:"disabled"`
175+
Disabled bool `yaml:"disabled" description:"Disables the code generation of the mock server target"`
138176
}
139177

140178
type LanguageConfig struct {
141-
Version string `yaml:"version"`
142-
Cfg map[string]any `yaml:",inline"`
179+
_ struct{} `additionalProperties:"true" description:"Language-specific SDK configuration"`
180+
Version string `yaml:"version" description:"SDK version"`
181+
Cfg map[string]any `yaml:",inline" jsonschema:"-"`
143182
}
144183

145184
type SDKGenConfigField struct {
@@ -157,9 +196,10 @@ type SDKGenConfigField struct {
157196

158197
// Ensure you update schema/gen.config.schema.json on changes
159198
type Configuration struct {
160-
ConfigVersion string `yaml:"configVersion"`
161-
Generation Generation `yaml:"generation"`
162-
Languages map[string]LanguageConfig `yaml:",inline"`
199+
_ struct{} `title:"Gen YAML Configuration Schema" additionalProperties:"false"`
200+
ConfigVersion string `yaml:"configVersion" description:"The version of the configuration file" minLength:"1" required:"true"`
201+
Generation Generation `yaml:"generation" required:"true"`
202+
Languages map[string]LanguageConfig `yaml:",inline" jsonschema:"-"`
163203
New map[string]bool `yaml:"-"`
164204
}
165205

configuration_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package config_test
2+
3+
//go:generate sh -c "cd tools/schema-gen && go run . -type config -out ../../schemas/gen.config.schema.json"
4+
5+
import (
6+
"bytes"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
"testing"
11+
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
// TestConfigSchemaInSync verifies that gen.config.schema.json is in sync with
16+
// what the schema generator produces from the Configuration struct.
17+
func TestConfigSchemaInSync(t *testing.T) {
18+
// Generate schema from current Go structs
19+
cmd := exec.Command("go", "run", ".", "-type", "config", "-out", "-")
20+
cmd.Dir = filepath.Join("tools", "schema-gen")
21+
22+
var stdout, stderr bytes.Buffer
23+
cmd.Stdout = &stdout
24+
cmd.Stderr = &stderr
25+
26+
err := cmd.Run()
27+
require.NoError(t, err, "schema generator failed: %s", stderr.String())
28+
29+
// Read the committed schema
30+
committedPath := filepath.Join("schemas", "gen.config.schema.json")
31+
committedBytes, err := os.ReadFile(committedPath)
32+
require.NoError(t, err, "Failed to read committed schema")
33+
34+
// Compare byte-for-byte
35+
generated := stdout.Bytes()
36+
require.Equal(t, string(committedBytes), string(generated),
37+
"Generated config schema does not match committed schemas/gen.config.schema.json.\n"+
38+
"Run: cd tools/schema-gen && go run . -type config -out ../../schemas/gen.config.schema.json\n"+
39+
"Then commit the updated file.")
40+
}

io.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"os"
1212
"path/filepath"
1313

14+
"github.com/speakeasy-api/sdk-gen-config/lockfile"
1415
"github.com/speakeasy-api/sdk-gen-config/workspace"
1516
"gopkg.in/yaml.v3"
1617
)
@@ -262,9 +263,18 @@ func Load(dir string, opts ...Option) (*Config, error) {
262263
return nil, fmt.Errorf("could not unmarshal gen.yaml: %w", err)
263264
}
264265

265-
var lockFile LockFile
266-
if err := yaml.Unmarshal(lockFileRes.Data, &lockFile); err != nil {
267-
return nil, fmt.Errorf("could not unmarshal gen.lock: %w", err)
266+
var lockOpts []lockfile.LoadOption
267+
if o.FS != nil {
268+
lockOpts = append(lockOpts, lockfile.WithFileSystem(o.FS))
269+
}
270+
271+
lock, err := lockfile.Load(lockFileRes.Data, lockOpts...)
272+
if err != nil {
273+
return nil, fmt.Errorf("could not parse gen.lock: %w", err)
274+
}
275+
276+
if o.FS != nil {
277+
_ = lockfile.PopulateMissingChecksums(lock, o.FS)
268278
}
269279

270280
cfg.New = newForLang
@@ -288,14 +298,14 @@ func Load(dir string, opts ...Option) (*Config, error) {
288298
}
289299
}
290300

291-
if lockFile.Features == nil {
292-
lockFile.Features = make(map[string]map[string]string)
301+
if lock.Features == nil {
302+
lock.Features = make(map[string]map[string]string)
293303
}
294304

295305
config := &Config{
296306
Config: cfg,
297307
ConfigPath: configRes.Path,
298-
LockFile: &lockFile,
308+
LockFile: lock,
299309
}
300310

301311
if o.transformerFunc != nil {

0 commit comments

Comments
 (0)