Skip to content
Draft
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
6 changes: 6 additions & 0 deletions internal/codegen/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/grafana/cog/internal/jennies/python"
"github.com/grafana/cog/internal/jennies/terraform"
"github.com/grafana/cog/internal/jennies/typescript"
"github.com/grafana/cog/internal/jennies/zod"
)

type Output struct {
Expand Down Expand Up @@ -69,6 +70,7 @@ type OutputLanguage struct {
Python *python.Config `yaml:"python"`
Typescript *typescript.Config `yaml:"typescript"`
Terraform *terraform.Config `yaml:"terraform"`
Zod *zod.Config `yaml:"zod"`
}

func (outputLanguage *OutputLanguage) interpolateParameters(output *Output, interpolator ParametersInterpolator) {
Expand Down Expand Up @@ -96,6 +98,10 @@ func (outputLanguage *OutputLanguage) interpolateParameters(output *Output, inte
if outputLanguage.Terraform != nil {
outputLanguage.Terraform.InterpolateParameters(interpolator)
}

if outputLanguage.Zod != nil {
outputLanguage.Zod.InterpolateParameters(interpolator)
}
}

type OutputOptions struct {
Expand Down
3 changes: 3 additions & 0 deletions internal/codegen/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/grafana/cog/internal/jennies/python"
"github.com/grafana/cog/internal/jennies/terraform"
"github.com/grafana/cog/internal/jennies/typescript"
"github.com/grafana/cog/internal/jennies/zod"
"github.com/grafana/cog/internal/languages"
"github.com/grafana/cog/internal/logs"
"github.com/grafana/cog/internal/tools"
Expand Down Expand Up @@ -321,6 +322,8 @@ func (pipeline *Pipeline) OutputLanguages() (languages.Languages, error) {
outputs[typescript.LanguageRef] = typescript.New(*output.Typescript)
case output.Terraform != nil:
outputs[terraform.LanguageRef] = terraform.New(*output.Terraform)
case output.Zod != nil:
outputs[zod.LanguageRef] = zod.New(*output.Zod)
default:
return nil, fmt.Errorf("empty language configuration")
}
Expand Down
118 changes: 118 additions & 0 deletions internal/jennies/zod/jennies.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package zod

import (
"github.com/grafana/codejen"
"github.com/grafana/cog/internal/ast/compiler"
"github.com/grafana/cog/internal/jennies/common"
"github.com/grafana/cog/internal/languages"
"github.com/grafana/cog/internal/tools"
)

const LanguageRef = "zod"

type Config struct {
// PathPrefix is the directory prefix for generated files. Defaults to "src".
PathPrefix *string `yaml:"path_prefix"`

// Filename overrides the per-package output filename. Defaults to
// "schemas.gen.ts".
Filename *string `yaml:"filename"`

// FlatLayout drops the per-package directory and writes directly into
// PathPrefix. Useful when the consumer manages its own layout.
FlatLayout bool `yaml:"flat_layout"`

// OptionalKindLiterals turns required fields whose type is a concrete
// string literal (e.g. `kind: "Panel"`) into `.optional().default("X")`,
// so callers don't have to repeat the discriminator. The literal still
// constrains values that are supplied. Implies VerboseDefaults so the
// generated file uses one form for every defaulted field.
OptionalKindLiterals bool `yaml:"optional_kind_literals"`

// ObjectMode controls how unknown keys are handled at parse time:
// - "strip" (default): unknown keys are silently dropped (Zod's default).
// - "strict": unknown keys cause parse errors.
// - "loose": unknown keys are kept on the parsed output.
ObjectMode string `yaml:"object_mode"`

// PreferUnknownForAny emits `z.unknown()` instead of `z.any()` for CUE's
// top type (`_`), forcing consumers to narrow before use.
PreferUnknownForAny bool `yaml:"prefer_unknown_for_any"`

// VerboseDefaults emits defaulted fields as `.optional().default(X)`
// rather than `.default(X).optional()`. The verbose form fixes the
// inferred output type, which would otherwise include `undefined` even
// though the default always fires. Implied by OptionalKindLiterals.
VerboseDefaults bool `yaml:"verbose_defaults"`
}

func (config *Config) InterpolateParameters(interpolator func(input string) string) {
if config.PathPrefix != nil {
config.PathPrefix = tools.ToPtr(interpolator(*config.PathPrefix))
}
if config.Filename != nil {
config.Filename = tools.ToPtr(interpolator(*config.Filename))
}
}

func (config *Config) applyDefaults() {
if config.PathPrefix == nil {
config.PathPrefix = tools.ToPtr("src")
}
if config.Filename == nil {
config.Filename = tools.ToPtr("schemas.gen.ts")
}
if config.ObjectMode == "" {
config.ObjectMode = "strip"
}
}

type Language struct {
config Config
}

func New(config Config) *Language {
return &Language{
config: config,
}
}

func (language *Language) Name() string {
return LanguageRef
}

func (language *Language) Jennies(globalConfig languages.Config) *codejen.JennyList[languages.Context] {
language.config.applyDefaults()

jenny := codejen.JennyListWithNamer(func(_ languages.Context) string {
return LanguageRef
})

jenny.AppendOneToMany(
common.If(globalConfig.Types, RawTypes{config: language.config}),
)

jenny.AddPostprocessors(common.GeneratedCommentHeader(globalConfig))

return jenny
}

func (language *Language) CompilerPasses() compiler.Passes {
// Order matters: each pass assumes the shape produced by the previous one.
return compiler.Passes{
// `T | null` → `T?` so we can attach .nullable() at the call site.
&compiler.DisjunctionWithNullToOptional{},
// `"a" | "b" | "c"` → enum, so we emit z.enum instead of a union of literals.
&compiler.DisjunctionOfConstantsToEnum{},
// Flatten before DisjunctionInferMapping, which needs flat disjunctions
// to spot a shared discriminator.
&compiler.FlattenDisjunctions{},
// Sets Discriminator on unions of struct refs that share a scalar field,
// enabling z.discriminatedUnion instead of z.union.
&compiler.DisjunctionInferMapping{},
}
}

func (language *Language) NullableKinds() languages.NullableConfig {
return languages.NullableConfig{}
}
Loading
Loading