Skip to content

Commit

Permalink
Rework duration support
Browse files Browse the repository at this point in the history
  • Loading branch information
ptodev committed Sep 19, 2024
1 parent b65abed commit b385088
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 58 deletions.
17 changes: 17 additions & 0 deletions pkg/codegen/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,23 @@ func (t NamedType) Generate(out *Emitter) {
out.Printf("%s", t.Decl.Name)
}

// We need special handling when validating durations.
// Having a dedicated type for durations allows the validator to
// differentiate between durations and other types.
type DurationType struct{}

func (t DurationType) GetName() string {
return "Duration"
}

func (t DurationType) IsNillable() bool {
return false
}

func (t DurationType) Generate(out *Emitter) {
out.Printf("time.Duration")
}

type PrimitiveType struct {
Type string
}
Expand Down
16 changes: 1 addition & 15 deletions pkg/codegen/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,21 +111,7 @@ func PrimitiveTypeFromJSONSchemaType(
}

case "duration":
t = NamedType{
Package: &Package{
QualifiedName: "time",
Imports: []Import{
{
//TODO: Should we use the new internal Duration from pkg/types?
// Are the Marshal/Unmarshal functions important?
QualifiedName: "time",
},
},
},
Decl: &TypeDecl{
Name: "Duration",
},
}
t = DurationType{}

default:
t = PrimitiveType{"string"}
Expand Down
14 changes: 14 additions & 0 deletions pkg/generator/schema_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,10 @@ func (g *schemaGenerator) generateDeclaredType(
func (g *schemaGenerator) addValidatorsToType(validators []validator, decl codegen.TypeDecl) {
if len(validators) > 0 {
for _, v := range validators {
for _, im := range v.desc().imports {
g.output.file.Package.AddImport(im.qualifiedName, im.alias)
}

if v.desc().hasError {
g.output.file.Package.AddImport("fmt", "")

Expand Down Expand Up @@ -462,6 +466,11 @@ func (g *schemaGenerator) generateType(
return ncg, nil
}

if dcg, ok := cg.(codegen.DurationType); ok {
g.output.file.Package.AddImport("time", "")
return dcg, nil
}

return cg, nil
}
}
Expand Down Expand Up @@ -746,6 +755,11 @@ func (g *schemaGenerator) generateTypeInline(
return ncg, nil
}

if dcg, ok := cg.(codegen.DurationType); ok {
g.output.file.Package.AddImport("time", "")
return dcg, nil
}

return cg, nil
}

Expand Down
58 changes: 57 additions & 1 deletion pkg/generator/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/pkg/errors"
"github.com/sanity-io/litter"
"github.com/sosodev/duration"

"github.com/atombender/go-jsonschema/pkg/codegen"
"github.com/atombender/go-jsonschema/pkg/mathutils"
Expand All @@ -17,10 +18,16 @@ type validator interface {
desc() *validatorDesc
}

type packageImport struct {
qualifiedName string
alias string
}

type validatorDesc struct {
hasError bool
beforeJSONUnmarshal bool
requiresRawAfter bool
imports []packageImport
}

var (
Expand Down Expand Up @@ -102,7 +109,7 @@ func (v *nullTypeValidator) desc() *validatorDesc {
}
}

//TODO: Make a durationValidator, so that we can convert the default value from ISO 8601 to time.Duration.
// TODO: Make a durationValidator, so that we can convert the default value from ISO 8601 to time.Duration.
// We could use https://github.com/sosodev/duration

type defaultValidator struct {
Expand All @@ -113,6 +120,42 @@ type defaultValidator struct {
}

func (v *defaultValidator) generate(out *codegen.Emitter) {
_, ok := v.defaultValueType.(codegen.DurationType)
if v.defaultValueType != nil && ok {
defaultDurationISO8601, ok := v.defaultValue.(string)
if !ok {
// TODO: Return an error instead of panicking?
// TODO: Print type name?
panic("duration default value must be a string")
}
if defaultDurationISO8601 == "" {
// TODO: What should we do if the default is an empty string?
// TODO: Return an error instead of panicking?
// TODO: Print type name?
panic("duration default value must not be an empty string")
}
duration, err := duration.Parse(defaultDurationISO8601)
if err != nil {
// TODO: Return an error instead of panicking?
// TODO: Print type name?
panic("could not convert duration from ISO8601 to Go format")
}
defaultValue := "defaultDuration"
goDurationStr := duration.ToTimeDuration().String()
out.Printlnf(`if v, ok := %s["%s"]; !ok || v == nil {`, varNameRawMap, v.jsonName)
out.Indent(1)
out.Printlnf("%s, err := time.ParseDuration(\"%s\")", defaultValue, goDurationStr)
out.Printlnf("if err != nil {")
out.Indent(1)
out.Printlnf("return fmt.Errorf(\"failed to parse the \\\"%s\\\" default value for field %s:%%w }\", err)", goDurationStr, v.jsonName)
out.Indent(-1)
out.Printlnf("}")
out.Printlnf(`%s.%s = %s`, varNamePlainStruct, v.fieldName, defaultValue)
out.Indent(-1)
out.Printlnf("}")
return
}

defaultValue, err := v.tryDumpDefaultSlice(out.MaxLineLength())
if err != nil {
// Fallback to sdump in case we couldn't dump it properly.
Expand Down Expand Up @@ -153,10 +196,23 @@ func (v *defaultValidator) tryDumpDefaultSlice(maxLineLen int32) (string, error)
}

func (v *defaultValidator) desc() *validatorDesc {
var packages []packageImport
_, ok := v.defaultValueType.(codegen.DurationType)
if v.defaultValueType != nil && ok {
defaultDurationISO8601, ok := v.defaultValue.(string)
if ok && defaultDurationISO8601 != "" {
packages = []packageImport{
{qualifiedName: "fmt"},
{qualifiedName: "time"},
}
}
}

return &validatorDesc{
hasError: false,
beforeJSONUnmarshal: false,
requiresRawAfter: true,
imports: packages,
}
}

Expand Down
34 changes: 0 additions & 34 deletions pkg/types/duration.go

This file was deleted.

30 changes: 28 additions & 2 deletions tests/data/core/duration/duration.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 6 additions & 5 deletions tests/data/core/duration/duration.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
"properties": {
"myObject": {
"type": "object",
"required": [
"myDuration"
],
"properties": {
"myDateTime": {
"withDefault": {
"type": "string",
"format": "duration",
"default": "P200MS"
"default": "PT20S"
},
"withoutDefault": {
"type": "string",
"format": "duration"
}
}
}
Expand Down
6 changes: 5 additions & 1 deletion tests/generation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ var (
func TestCore(t *testing.T) {
t.Parallel()

// TODO: Additional duration tests:
// * A duration with default = ""
// * A schema file with only one duration.
// It shouldn't have a default, so we ca see if "time" is imported.
testExamples(t, basicConfig, "./data/core")
}

Expand Down Expand Up @@ -243,7 +247,7 @@ func testExampleFile(t *testing.T, cfg generator.Config, fileName string) {
}

if diff, ok := diffStrings(t, string(goldenData), string(source)); !ok {
//TODO: Revert this later
// TODO: Revert this later
// t.Fatalf("Contents different (left is expected, right is actual):\n%s", *diff)
t.Fatalf("Contents different. Actual:\n%s\nDiff:\n%s", string(source), *diff)
}
Expand Down

0 comments on commit b385088

Please sign in to comment.