Skip to content
This repository was archived by the owner on Mar 17, 2026. It is now read-only.

Commit 94c5aed

Browse files
authored
fix(cli): make subtypes conditionally required (#126)
1 parent 11e2a5e commit 94c5aed

4 files changed

Lines changed: 143 additions & 4 deletions

File tree

cli/pkg/app/suga.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ func (c *SugaApp) Build() error {
465465
return nil
466466
}
467467

468-
appSpec, err := schema.LoadFromFile(c.fs, version.ConfigFileName, true)
468+
appSpec, err := schema.LoadFromFile(c.fs, version.ConfigFileName, true, schema.WithRequireSubtypes())
469469
if err != nil {
470470
return err
471471
}

cli/pkg/schema/file.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"gopkg.in/yaml.v3"
1111
)
1212

13-
func LoadFromFile(fs afero.Fs, path string, validate bool) (*Application, error) {
13+
func LoadFromFile(fs afero.Fs, path string, validate bool, validationOpts ...ValidationOption) (*Application, error) {
1414
if exists, err := afero.Exists(fs, path); err != nil {
1515
return nil, fmt.Errorf("%s application file could not be loaded at path: %s", version.ProductName, path)
1616
} else if !exists {
@@ -46,7 +46,7 @@ func LoadFromFile(fs afero.Fs, path string, validate bool) (*Application, error)
4646
validationErrors = append(validationErrors, GetSchemaValidationErrors(results.Errors())...)
4747
}
4848

49-
if appSpecErrors := appSpec.IsValid(); len(appSpecErrors) > 0 {
49+
if appSpecErrors := appSpec.IsValid(validationOpts...); len(appSpecErrors) > 0 {
5050
validationErrors = append(validationErrors, GetSchemaValidationErrors(appSpecErrors)...)
5151
}
5252

cli/pkg/schema/schema_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,3 +518,85 @@ websites:
518518
assert.Contains(t, errString, "main-api: # <-- entrypoint name main-api must be in snake_case format")
519519
assert.Contains(t, errString, "user-db: # <-- database name user-db must be in snake_case format")
520520
}
521+
522+
func TestApplication_IsValid_SubtypesOptional(t *testing.T) {
523+
app := &Application{
524+
Name: "test-app",
525+
Target: "team/platform@1",
526+
ServiceIntents: map[string]*ServiceIntent{
527+
"api": {
528+
Container: Container{
529+
Docker: &Docker{Dockerfile: "Dockerfile"},
530+
},
531+
},
532+
},
533+
BucketIntents: map[string]*BucketIntent{
534+
"storage": {},
535+
},
536+
EntrypointIntents: map[string]*EntrypointIntent{
537+
"main": {
538+
Routes: map[string]Route{
539+
"/": {TargetName: "api"},
540+
},
541+
},
542+
},
543+
DatabaseIntents: map[string]*DatabaseIntent{
544+
"users": {EnvVarKey: "DATABASE_URL"},
545+
},
546+
}
547+
548+
// Without WithRequireSubtypes, validation should pass
549+
violations := app.IsValid()
550+
assert.Len(t, violations, 0, "Expected no violations without RequireSubtypes option, got: %v", violations)
551+
552+
// With WithRequireSubtypes, validation should fail
553+
violations = app.IsValid(WithRequireSubtypes())
554+
assert.NotEmpty(t, violations, "Expected violations with RequireSubtypes option")
555+
556+
errString := FormatValidationErrors(GetSchemaValidationErrors(violations))
557+
assert.Contains(t, errString, "api: # <-- service must have a subtype specified for build")
558+
assert.Contains(t, errString, "storage: # <-- bucket must have a subtype specified for build")
559+
assert.Contains(t, errString, "main: # <-- entrypoint must have a subtype specified for build")
560+
assert.Contains(t, errString, "users: # <-- database must have a subtype specified for build")
561+
}
562+
563+
func TestApplication_IsValid_WithSubtypes(t *testing.T) {
564+
app := &Application{
565+
Name: "test-app",
566+
Target: "team/platform@1",
567+
ServiceIntents: map[string]*ServiceIntent{
568+
"api": {
569+
Resource: Resource{SubType: "fargate"},
570+
Container: Container{
571+
Docker: &Docker{Dockerfile: "Dockerfile"},
572+
},
573+
},
574+
},
575+
BucketIntents: map[string]*BucketIntent{
576+
"storage": {
577+
Resource: Resource{SubType: "s3"},
578+
},
579+
},
580+
EntrypointIntents: map[string]*EntrypointIntent{
581+
"main": {
582+
Resource: Resource{SubType: "alb"},
583+
Routes: map[string]Route{
584+
"/": {TargetName: "api"},
585+
},
586+
},
587+
},
588+
DatabaseIntents: map[string]*DatabaseIntent{
589+
"users": {
590+
Resource: Resource{SubType: "postgres"},
591+
EnvVarKey: "DATABASE_URL",
592+
},
593+
},
594+
}
595+
596+
// With subtypes specified, validation should pass with or without RequireSubtypes
597+
violations := app.IsValid()
598+
assert.Len(t, violations, 0, "Expected no violations without RequireSubtypes option, got: %v", violations)
599+
600+
violations = app.IsValid(WithRequireSubtypes())
601+
assert.Len(t, violations, 0, "Expected no violations with RequireSubtypes option, got: %v", violations)
602+
}

cli/pkg/schema/validate.go

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,31 @@ import (
99
"github.com/xeipuuv/gojsonschema"
1010
)
1111

12+
// ValidationOptions defines options for application validation
13+
type ValidationOptions struct {
14+
RequireSubtypes bool
15+
}
16+
17+
// ValidationOption is a function that modifies ValidationOptions
18+
type ValidationOption func(*ValidationOptions)
19+
20+
// WithRequireSubtypes enables validation that requires all resources to have subtypes
21+
func WithRequireSubtypes() ValidationOption {
22+
return func(opts *ValidationOptions) {
23+
opts.RequireSubtypes = true
24+
}
25+
}
26+
1227
// Perform additional validation checks on the application
13-
func (a *Application) IsValid() []gojsonschema.ResultError {
28+
func (a *Application) IsValid(options ...ValidationOption) []gojsonschema.ResultError {
29+
opts := &ValidationOptions{
30+
RequireSubtypes: false, // default: subtypes are optional
31+
}
32+
33+
for _, option := range options {
34+
option(opts)
35+
}
36+
1437
// Check the names of all resources are unique
1538
violations := a.checkNoNameConflicts()
1639
violations = append(violations, a.checkNoReservedNames()...)
@@ -19,6 +42,10 @@ func (a *Application) IsValid() []gojsonschema.ResultError {
1942
violations = append(violations, a.checkAccessPermissions()...)
2043
violations = append(violations, a.checkNoRedundantEntrypointRoutes()...)
2144

45+
if opts.RequireSubtypes {
46+
violations = append(violations, a.checkSubtypesRequired()...)
47+
}
48+
2249
return violations
2350
}
2451

@@ -199,3 +226,33 @@ func (a *Application) checkNoRedundantEntrypointRoutes() []gojsonschema.ResultEr
199226

200227
return violations
201228
}
229+
230+
func (a *Application) checkSubtypesRequired() []gojsonschema.ResultError {
231+
violations := []gojsonschema.ResultError{}
232+
233+
for name, intent := range a.ServiceIntents {
234+
if intent.GetSubType() == "" {
235+
violations = append(violations, newValidationError(fmt.Sprintf("services.%s", name), "service must have a subtype specified for build"))
236+
}
237+
}
238+
239+
for name, intent := range a.BucketIntents {
240+
if intent.GetSubType() == "" {
241+
violations = append(violations, newValidationError(fmt.Sprintf("buckets.%s", name), "bucket must have a subtype specified for build"))
242+
}
243+
}
244+
245+
for name, intent := range a.EntrypointIntents {
246+
if intent.GetSubType() == "" {
247+
violations = append(violations, newValidationError(fmt.Sprintf("entrypoints.%s", name), "entrypoint must have a subtype specified for build"))
248+
}
249+
}
250+
251+
for name, intent := range a.DatabaseIntents {
252+
if intent.GetSubType() == "" {
253+
violations = append(violations, newValidationError(fmt.Sprintf("databases.%s", name), "database must have a subtype specified for build"))
254+
}
255+
}
256+
257+
return violations
258+
}

0 commit comments

Comments
 (0)