@@ -6,9 +6,11 @@ import (
66 "errors"
77 "fmt"
88 "reflect"
9+ "regexp"
910 "strconv"
1011 "strings"
1112 "sync"
13+ "time"
1214
1315 "github.com/iancoleman/strcase"
1416
@@ -296,7 +298,24 @@ func (u *Unmarshaler) setDefaultValue(field reflect.Value, defaultVal string) er
296298 }
297299 field .SetBool (b )
298300 default :
299- return fmt .Errorf ("unsupported default value type: %v" , field .Kind ())
301+ // Handle special types that don't match basic kinds
302+ switch field .Type () {
303+ case reflect .TypeOf (time .Duration (0 )):
304+ duration , err := time .ParseDuration (defaultVal )
305+ if err != nil {
306+ return fmt .Errorf ("cannot parse default duration %q: %w" , defaultVal , err )
307+ }
308+ field .Set (reflect .ValueOf (duration ))
309+ case reflect .TypeOf (time.Time {}):
310+ // Try multiple time formats for default values
311+ parsedTime , err := parseTimeValue (defaultVal , "" )
312+ if err != nil {
313+ return fmt .Errorf ("cannot parse default time %q: %w" , defaultVal , err )
314+ }
315+ field .Set (reflect .ValueOf (parsedTime ))
316+ default :
317+ return fmt .Errorf ("unsupported default value type: %v" , field .Type ())
318+ }
300319 }
301320
302321 return nil
@@ -312,7 +331,7 @@ func (u *Unmarshaler) unmarshalValue(ctx context.Context, val format.Value, fiel
312331 case * numberData :
313332 return u .unmarshalNumber (v , field )
314333 case * stringData :
315- return u .unmarshalString (ctx , v , field )
334+ return u .unmarshalString (ctx , v , field , structField )
316335 case Array :
317336 if field .Type ().Implements (reflect .TypeOf ((* format .Value )(nil )).Elem ()) {
318337 field .Set (reflect .ValueOf (v ))
@@ -336,37 +355,225 @@ func (u *Unmarshaler) unmarshalValue(ctx context.Context, val format.Value, fiel
336355 }
337356}
338357
358+ // parseInstillTag parses the instill tag and returns field name, format, pattern, and other attributes
359+ func parseInstillTag (tag string ) (fieldName , format , pattern string , attributes map [string ]string ) {
360+ attributes = make (map [string ]string )
361+ if tag == "" {
362+ return
363+ }
364+
365+ // First, extract the field name (everything before the first comma)
366+ firstCommaIdx := strings .Index (tag , "," )
367+ if firstCommaIdx == - 1 {
368+ fieldName = tag
369+ return
370+ }
371+
372+ fieldName = tag [:firstCommaIdx ]
373+ remaining := tag [firstCommaIdx + 1 :]
374+
375+ // Parse the remaining attributes using a simple approach that handles patterns better
376+ parts := strings .Split (remaining , "," )
377+
378+ for i := 0 ; i < len (parts ); i ++ {
379+ part := strings .TrimSpace (parts [i ])
380+ if part == "" {
381+ continue
382+ }
383+
384+ switch {
385+ case strings .HasPrefix (part , "default=" ):
386+ attributes ["default" ] = strings .TrimPrefix (part , "default=" )
387+ case strings .HasPrefix (part , "pattern=" ):
388+ // For patterns, we may need to rejoin parts if the pattern contains commas
389+ patternValue := strings .TrimPrefix (part , "pattern=" )
390+ // Check if this looks like an incomplete regex (missing closing bracket/paren)
391+ if strings .Contains (patternValue , "(" ) && ! strings .Contains (patternValue , ")" ) && i + 1 < len (parts ) {
392+ // Likely a pattern split by comma, rejoin with next parts until we find a closing paren or end
393+ for j := i + 1 ; j < len (parts ); j ++ {
394+ patternValue += "," + parts [j ]
395+ if strings .Contains (parts [j ], ")" ) {
396+ i = j // Skip the parts we've consumed
397+ break
398+ }
399+ }
400+ }
401+ pattern = patternValue
402+ case strings .HasPrefix (part , "format=" ):
403+ format = strings .TrimPrefix (part , "format=" )
404+ case strings .Contains (part , "/" ) && ! strings .Contains (part , "=" ):
405+ // Legacy format specification without "format=" prefix
406+ format = part
407+ }
408+ }
409+
410+ return
411+ }
412+
413+ // validatePattern validates a string against a regex pattern
414+ func validatePattern (value , pattern string ) error {
415+ if pattern == "" {
416+ return nil
417+ }
418+
419+ // Unescape the pattern (convert \\. to \.)
420+ unescapedPattern := strings .ReplaceAll (pattern , "\\ \\ " , "\\ " )
421+
422+ regex , err := regexp .Compile (unescapedPattern )
423+ if err != nil {
424+ return fmt .Errorf ("invalid pattern %q: %w" , pattern , err )
425+ }
426+
427+ if ! regex .MatchString (value ) {
428+ return fmt .Errorf ("value %q does not match pattern %q" , value , pattern )
429+ }
430+
431+ return nil
432+ }
433+
434+ // parseTimeValue parses a time string using appropriate formats based on the format hint
435+ func parseTimeValue (timeStr , format string ) (time.Time , error ) {
436+ var timeFormats []string
437+
438+ // If format is "date-time" or similar, prioritize RFC3339 formats
439+ if format == "date-time" || format == "datetime" {
440+ timeFormats = []string {
441+ time .RFC3339 ,
442+ time .RFC3339Nano ,
443+ }
444+ } else {
445+ // Try multiple time formats
446+ timeFormats = []string {
447+ time .RFC3339 ,
448+ time .RFC3339Nano ,
449+ "2006-01-02T15:04:05Z07:00" ,
450+ "2006-01-02 15:04:05" ,
451+ "2006-01-02" ,
452+ }
453+ }
454+
455+ for _ , timeFormat := range timeFormats {
456+ if parsedTime , err := time .Parse (timeFormat , timeStr ); err == nil {
457+ return parsedTime , nil
458+ }
459+ }
460+
461+ return time.Time {}, fmt .Errorf ("unable to parse time string with any supported format" )
462+ }
463+
464+ // isFileType checks if a type is a file-related format type
465+ func isFileType (t reflect.Type ) bool {
466+ fileTypes := []reflect.Type {
467+ reflect .TypeOf ((* format .Image )(nil )).Elem (),
468+ reflect .TypeOf ((* format .Audio )(nil )).Elem (),
469+ reflect .TypeOf ((* format .Video )(nil )).Elem (),
470+ reflect .TypeOf ((* format .Document )(nil )).Elem (),
471+ reflect .TypeOf ((* format .File )(nil )).Elem (),
472+ }
473+
474+ for _ , fileType := range fileTypes {
475+ if t == fileType {
476+ return true
477+ }
478+ }
479+ return false
480+ }
481+
482+ // handleTimePointer handles marshaling of time pointer types
483+ func handleTimePointer (v reflect.Value ) (format.Value , bool ) {
484+ elemType := v .Type ().Elem ()
485+ switch elemType {
486+ case reflect .TypeOf (time.Time {}):
487+ timeVal := v .Interface ().(* time.Time )
488+ return NewString (timeVal .Format (time .RFC3339 )), true
489+ case reflect .TypeOf (time .Duration (0 )):
490+ durationVal := v .Interface ().(* time.Duration )
491+ return NewString (durationVal .String ()), true
492+ }
493+ return nil , false
494+ }
495+
339496// unmarshalString handles unmarshaling of String values.
340- func (u * Unmarshaler ) unmarshalString (ctx context.Context , v format.String , field reflect.Value ) error {
497+ func (u * Unmarshaler ) unmarshalString (ctx context.Context , v format.String , field reflect.Value , structField reflect.StructField ) error {
498+ stringValue := v .String ()
499+
500+ // Parse instill tag for validation rules
501+ _ , _ , pattern , _ := parseInstillTag (structField .Tag .Get ("instill" ))
502+
503+ // Validate against pattern if specified (applies to all string fields)
504+ if err := validatePattern (stringValue , pattern ); err != nil {
505+ return fmt .Errorf ("pattern validation failed: %w" , err )
506+ }
507+
341508 switch field .Kind () {
342509 case reflect .String :
343- field .SetString (v . String () )
510+ field .SetString (stringValue )
344511 case reflect .Ptr :
345512 if field .IsNil () {
346513 field .Set (reflect .New (field .Type ().Elem ()))
347514 }
348- return u .unmarshalString (ctx , v , field .Elem ())
515+ return u .unmarshalString (ctx , v , field .Elem (), structField )
349516 default :
350517 switch field .Type () {
518+ // Handle time.Duration
519+ case reflect .TypeOf (time .Duration (0 )):
520+ // Parse instill tag for parsing hints
521+ _ , _ , pattern , _ := parseInstillTag (structField .Tag .Get ("instill" ))
522+
523+ // If pattern suggests seconds format, parse as seconds
524+ if pattern != "" && strings .Contains (pattern , "s$" ) {
525+ // Pattern suggests seconds format like "3600s" or "3600.5s"
526+ // Remove the 's' suffix and parse as float, then convert to duration
527+ if strings .HasSuffix (stringValue , "s" ) {
528+ secondsStr := strings .TrimSuffix (stringValue , "s" )
529+ seconds , err := strconv .ParseFloat (secondsStr , 64 )
530+ if err != nil {
531+ return fmt .Errorf ("cannot parse seconds value %q: %w" , secondsStr , err )
532+ }
533+ duration := time .Duration (seconds * float64 (time .Second ))
534+ field .Set (reflect .ValueOf (duration ))
535+ } else {
536+ return fmt .Errorf ("duration string %q does not end with 's' as required by pattern" , stringValue )
537+ }
538+ } else {
539+ // No pattern or different pattern, use standard Go duration parsing
540+ duration , err := time .ParseDuration (stringValue )
541+ if err != nil {
542+ return fmt .Errorf ("cannot unmarshal string %q into time.Duration: %w" , stringValue , err )
543+ }
544+ field .Set (reflect .ValueOf (duration ))
545+ }
546+ // Handle time.Time
547+ case reflect .TypeOf (time.Time {}):
548+ // Parse instill tag for format specification
549+ _ , format , _ , _ := parseInstillTag (structField .Tag .Get ("instill" ))
351550
352- // If the string is a URL, create a file from the URL
353- case reflect .TypeOf ((* format .Image )(nil )).Elem (),
354- reflect .TypeOf ((* format .Audio )(nil )).Elem (),
355- reflect .TypeOf ((* format .Video )(nil )).Elem (),
356- reflect .TypeOf ((* format .Document )(nil )).Elem (),
357- reflect .TypeOf ((* format .File )(nil )).Elem ():
358- f , err := u .createFileFromURL (ctx , field .Type (), v .String ())
359- if err == nil {
360- field .Set (reflect .ValueOf (f ))
361- return nil
551+ parsedTime , err := parseTimeValue (stringValue , format )
552+ if err != nil {
553+ return fmt .Errorf ("cannot unmarshal string %q into time.Time: %w" , stringValue , err )
362554 }
363- // If URL creation fails, return a helpful error message
364- return fmt .Errorf ("cannot unmarshal string into %v: expected valid URL, not base64 string: %w" , field .Type (), err )
365- case reflect .TypeOf (v ), reflect .TypeOf ((* format .String )(nil )).Elem ():
366- field .Set (reflect .ValueOf (v ))
367- case reflect .TypeOf ((* format .Value )(nil )).Elem ():
368- field .Set (reflect .ValueOf (v ))
555+ field .Set (reflect .ValueOf (parsedTime ))
556+
369557 default :
558+ // Try to create file from URL for media/document types
559+ if isFileType (field .Type ()) {
560+ f , err := u .createFileFromURL (ctx , field .Type (), v .String ())
561+ if err == nil {
562+ field .Set (reflect .ValueOf (f ))
563+ return nil
564+ }
565+ // If URL creation fails, return a helpful error message
566+ return fmt .Errorf ("cannot unmarshal string into %v: expected valid URL, not base64 string: %w" , field .Type (), err )
567+ }
568+
569+ // Handle format.Value types
570+ if field .Type () == reflect .TypeOf (v ) ||
571+ field .Type () == reflect .TypeOf ((* format .String )(nil )).Elem () ||
572+ field .Type () == reflect .TypeOf ((* format .Value )(nil )).Elem () {
573+ field .Set (reflect .ValueOf (v ))
574+ return nil
575+ }
576+
370577 return fmt .Errorf ("cannot unmarshal String into %v" , field .Type ())
371578 }
372579 }
@@ -418,10 +625,18 @@ func (u *Unmarshaler) unmarshalNumber(v format.Number, field reflect.Value) erro
418625 case reflect .Float32 , reflect .Float64 :
419626 field .SetFloat (v .Float64 ())
420627 case reflect .Int , reflect .Int8 , reflect .Int16 , reflect .Int32 , reflect .Int64 :
628+ // Special handling for time.Duration - should only accept string format
629+ if field .Type () == reflect .TypeOf (time .Duration (0 )) {
630+ return fmt .Errorf ("cannot unmarshal Number into time.Duration: use string format like \" 60s\" " )
631+ }
421632 field .SetInt (int64 (v .Integer ()))
422633 case reflect .Uint , reflect .Uint8 , reflect .Uint16 , reflect .Uint32 , reflect .Uint64 :
423634 field .SetUint (uint64 (v .Integer ()))
424635 case reflect .Ptr :
636+ // Special handling for *time.Duration - should only accept string format
637+ if field .Type ().Elem () == reflect .TypeOf (time .Duration (0 )) {
638+ return fmt .Errorf ("cannot unmarshal Number into *time.Duration: use string format like \" 60s\" " )
639+ }
425640 if field .IsNil () {
426641 field .Set (reflect .New (field .Type ().Elem ()))
427642 }
@@ -733,6 +948,13 @@ func (m *Marshaler) marshalValue(v reflect.Value) (format.Value, error) {
733948 }
734949 }
735950
951+ // Handle special pointer types before dereferencing
952+ if v .Kind () == reflect .Ptr && ! v .IsNil () {
953+ if timeVal , ok := handleTimePointer (v ); ok {
954+ return timeVal , nil
955+ }
956+ }
957+
736958 // Dereference pointer if necessary
737959 for v .Kind () == reflect .Ptr {
738960 if v .IsNil () {
@@ -743,7 +965,15 @@ func (m *Marshaler) marshalValue(v reflect.Value) (format.Value, error) {
743965
744966 switch v .Kind () {
745967 case reflect .Struct :
746- return m .marshalStruct (v )
968+ // Handle special struct types before generic struct marshaling
969+ switch v .Type () {
970+ case reflect .TypeOf (time.Time {}):
971+ // Marshal time.Time as RFC3339 string
972+ timeVal := v .Interface ().(time.Time )
973+ return NewString (timeVal .Format (time .RFC3339 )), nil
974+ default :
975+ return m .marshalStruct (v )
976+ }
747977 case reflect .Map :
748978 if v .Type ().Key ().Kind () != reflect .String {
749979 return nil , fmt .Errorf ("map key must be string type" )
@@ -754,6 +984,11 @@ func (m *Marshaler) marshalValue(v reflect.Value) (format.Value, error) {
754984 case reflect .Float32 , reflect .Float64 :
755985 return NewNumberFromFloat (v .Float ()), nil
756986 case reflect .Int , reflect .Int8 , reflect .Int16 , reflect .Int32 , reflect .Int64 :
987+ // Handle time.Duration before generic int64 handling
988+ if v .Type () == reflect .TypeOf (time .Duration (0 )) {
989+ durationVal := v .Interface ().(time.Duration )
990+ return NewString (durationVal .String ()), nil
991+ }
757992 return NewNumberFromInteger (int (v .Int ())), nil
758993 case reflect .Uint , reflect .Uint8 , reflect .Uint16 , reflect .Uint32 , reflect .Uint64 :
759994 return NewNumberFromInteger (int (v .Uint ())), nil
0 commit comments