diff --git a/flag.go b/flag.go index 1b849f1228..3cb9ab9608 100644 --- a/flag.go +++ b/flag.go @@ -151,6 +151,23 @@ type DocGenerationMultiValueFlag interface { IsMultiValueFlag() bool } +// SchemaTyper is an optional interface for flags that can report their +// JSON Schema type for programmatic introspection. +type SchemaTyper interface { + // SchemaType returns the JSON Schema type name for the value this + // flag accepts: "boolean", "integer", "number", "string", "array", + // "object". Returns "" if the flag does not map cleanly. + SchemaType() string +} + +// SchemaItemsTyper is an optional interface for multi-value flags that +// can report the JSON Schema type of their elements. +type SchemaItemsTyper interface { + // SchemaItemsType returns the JSON Schema type of elements for + // array-type flags. Returns "" for single-value or object flags. + SchemaItemsType() string +} + // Countable is an interface to enable detection of flag values which support // repetitive flags type Countable interface { diff --git a/flag_bool_with_inverse.go b/flag_bool_with_inverse.go index bc12c25a25..199d22c7d9 100644 --- a/flag_bool_with_inverse.go +++ b/flag_bool_with_inverse.go @@ -263,3 +263,11 @@ func (bif *BoolWithInverseFlag) IsDefaultVisible() bool { func (bif *BoolWithInverseFlag) TypeName() string { return "bool" } + +func (bif *BoolWithInverseFlag) SchemaType() string { + return "boolean" +} + +func (bif *BoolWithInverseFlag) SchemaItemsType() string { + return "" +} diff --git a/flag_ext.go b/flag_ext.go index 9972af7c56..25db27f16d 100644 --- a/flag_ext.go +++ b/flag_ext.go @@ -61,3 +61,11 @@ func (e *extFlag) GetDefaultText() string { func (e *extFlag) GetEnvVars() []string { return nil } + +func (e *extFlag) SchemaType() string { + return "" +} + +func (e *extFlag) SchemaItemsType() string { + return "" +} diff --git a/flag_impl.go b/flag_impl.go index ab97da8738..b6c7697cf7 100644 --- a/flag_impl.go +++ b/flag_impl.go @@ -6,6 +6,7 @@ import ( "fmt" "reflect" "strings" + "time" ) // Value represents a value as used by cli. @@ -285,6 +286,51 @@ func (f *FlagBase[T, C, V]) RunAction(ctx context.Context, cmd *Command) error { return nil } +// SchemaType returns the JSON Schema type for the flag's value type. +func (f *FlagBase[T, C, V]) SchemaType() string { + var zero T + switch any(zero).(type) { + case bool: + return "boolean" + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + return "integer" + case float32, float64: + return "number" + case string: + return "string" + case time.Duration: + return "string" + case time.Time: + return "string" + case []string, []int, []int8, []int16, []int32, []int64, + []uint, []uint8, []uint16, []uint32, []uint64, + []float32, []float64: + return "array" + case map[string]string: + return "object" + default: + return "" + } +} + +// SchemaItemsType returns the JSON Schema element type for slice flags. +func (f *FlagBase[T, C, V]) SchemaItemsType() string { + var zero T + t := reflect.TypeOf(zero) + if t.Kind() == reflect.Slice { + switch t.Elem().Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return "integer" + case reflect.Float32, reflect.Float64: + return "number" + case reflect.String: + return "string" + } + } + return "" +} + // IsMultiValueFlag returns true if the value type T can take multiple // values from cmd line. This is true for slice and map type flags func (f *FlagBase[T, C, VC]) IsMultiValueFlag() bool { diff --git a/flag_schema_type_test.go b/flag_schema_type_test.go new file mode 100644 index 0000000000..c5e81f55e2 --- /dev/null +++ b/flag_schema_type_test.go @@ -0,0 +1,161 @@ +package cli + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestFlag_SchemaType_Bool(t *testing.T) { + f := &BoolFlag{} + st, ok := any(f).(SchemaTyper) + assert.True(t, ok) + assert.Equal(t, "boolean", st.SchemaType()) + _, ok = any(f).(SchemaItemsTyper) + assert.True(t, ok) +} + +func TestFlag_SchemaType_String(t *testing.T) { + f := &StringFlag{} + st, ok := any(f).(SchemaTyper) + assert.True(t, ok) + assert.Equal(t, "string", st.SchemaType()) + _, ok = any(f).(SchemaItemsTyper) + assert.True(t, ok) +} + +func TestFlag_SchemaType_Int(t *testing.T) { + flags := []Flag{&IntFlag{}, &Int8Flag{}, &Int16Flag{}, &Int32Flag{}, &Int64Flag{}} + for _, f := range flags { + st, ok := f.(SchemaTyper) + assert.True(t, ok) + assert.Equal(t, "integer", st.SchemaType()) + } +} + +func TestFlag_SchemaType_Uint(t *testing.T) { + flags := []Flag{&UintFlag{}, &Uint8Flag{}, &Uint16Flag{}, &Uint32Flag{}, &Uint64Flag{}} + for _, f := range flags { + st, ok := f.(SchemaTyper) + assert.True(t, ok) + assert.Equal(t, "integer", st.SchemaType()) + } +} + +func TestFlag_SchemaType_Float(t *testing.T) { + flags := []Flag{&FloatFlag{}, &Float32Flag{}, &Float64Flag{}} + for _, f := range flags { + st, ok := f.(SchemaTyper) + assert.True(t, ok) + assert.Equal(t, "number", st.SchemaType()) + } +} + +func TestFlag_SchemaType_Duration(t *testing.T) { + f := &DurationFlag{} + st, ok := any(f).(SchemaTyper) + assert.True(t, ok) + assert.Equal(t, "string", st.SchemaType()) +} + +func TestFlag_SchemaType_Timestamp(t *testing.T) { + f := &TimestampFlag{} + st, ok := any(f).(SchemaTyper) + assert.True(t, ok) + assert.Equal(t, "string", st.SchemaType()) +} + +func TestFlag_SchemaType_Slice(t *testing.T) { + flags := []Flag{ + &StringSliceFlag{}, + &IntSliceFlag{}, + &FloatSliceFlag{}, + } + for _, f := range flags { + st, ok := f.(SchemaTyper) + assert.True(t, ok) + assert.Equal(t, "array", st.SchemaType()) + } +} + +func TestFlag_SchemaItemsType_Slice(t *testing.T) { + tests := []struct { + flag Flag + itemType string + }{ + {&StringSliceFlag{}, "string"}, + {&IntSliceFlag{}, "integer"}, + {&Int8SliceFlag{}, "integer"}, + {&Int16SliceFlag{}, "integer"}, + {&Int32SliceFlag{}, "integer"}, + {&Int64SliceFlag{}, "integer"}, + {&UintSliceFlag{}, "integer"}, + {&Uint8SliceFlag{}, "integer"}, + {&Uint16SliceFlag{}, "integer"}, + {&Uint32SliceFlag{}, "integer"}, + {&Uint64SliceFlag{}, "integer"}, + {&FloatSliceFlag{}, "number"}, + {&Float32SliceFlag{}, "number"}, + {&Float64SliceFlag{}, "number"}, + } + for _, tc := range tests { + t.Run("", func(t *testing.T) { + st, ok := tc.flag.(SchemaItemsTyper) + assert.True(t, ok) + assert.Equal(t, tc.itemType, st.SchemaItemsType()) + }) + } +} + +func TestFlag_SchemaType_Map(t *testing.T) { + f := &StringMapFlag{} + st, ok := any(f).(SchemaTyper) + assert.True(t, ok) + assert.Equal(t, "object", st.SchemaType()) + sit, ok := any(f).(SchemaItemsTyper) + assert.True(t, ok) + assert.Equal(t, "", sit.SchemaItemsType()) +} + +func TestFlag_SchemaType_Generic(t *testing.T) { + f := &GenericFlag{} + st, ok := any(f).(SchemaTyper) + assert.True(t, ok) + assert.Equal(t, "", st.SchemaType()) +} + +func TestFlag_SchemaType_BoolWithInverse(t *testing.T) { + f := &BoolWithInverseFlag{} + st, ok := any(f).(SchemaTyper) + assert.True(t, ok) + assert.Equal(t, "boolean", st.SchemaType()) + sit, ok := any(f).(SchemaItemsTyper) + assert.True(t, ok) + assert.Equal(t, "", sit.SchemaItemsType()) +} + +func TestFlag_SchemaType_NonSliceItemsType(t *testing.T) { + flags := []Flag{ + &BoolFlag{}, + &StringFlag{}, + &IntFlag{}, + &FloatFlag{}, + &DurationFlag{}, + &TimestampFlag{}, + } + for _, f := range flags { + sit, ok := f.(SchemaItemsTyper) + assert.True(t, ok) + assert.Equal(t, "", sit.SchemaItemsType()) + } +} + +func TestFlag_SchemaType_PreservesPrecision(t *testing.T) { + created := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) + f := &TimestampFlag{Config: TimestampConfig{Layouts: []string{time.RFC3339}}, Value: created} + assert.Equal(t, "string", f.SchemaType()) + + f2 := &DurationFlag{Value: 5 * time.Second} + assert.Equal(t, "string", f2.SchemaType()) +} diff --git a/flag_test.go b/flag_test.go index 7d7a80b359..955344c6ca 100644 --- a/flag_test.go +++ b/flag_test.go @@ -3312,6 +3312,9 @@ func TestExtFlag(t *testing.T) { assert.Equal(t, "11", extF.GetValue()) assert.Equal(t, "10", extF.GetDefaultText()) assert.Nil(t, extF.GetEnvVars()) + + assert.Equal(t, "", extF.SchemaType()) + assert.Equal(t, "", extF.SchemaItemsType()) } func TestSliceValuesNil(t *testing.T) { diff --git a/godoc-current.txt b/godoc-current.txt index 681df19d63..ff52914972 100644 --- a/godoc-current.txt +++ b/godoc-current.txt @@ -405,6 +405,10 @@ func (bif *BoolWithInverseFlag) PreParse() error func (bif *BoolWithInverseFlag) RunAction(ctx context.Context, cmd *Command) error +func (bif *BoolWithInverseFlag) SchemaItemsType() string + +func (bif *BoolWithInverseFlag) SchemaType() string + func (bif *BoolWithInverseFlag) Set(name, val string) error func (bif *BoolWithInverseFlag) SetCategory(c string) @@ -1032,6 +1036,12 @@ func (f *FlagBase[T, C, V]) PreParse() error func (f *FlagBase[T, C, V]) RunAction(ctx context.Context, cmd *Command) error RunAction executes flag action if set +func (f *FlagBase[T, C, V]) SchemaItemsType() string + SchemaItemsType returns the JSON Schema element type for slice flags. + +func (f *FlagBase[T, C, V]) SchemaType() string + SchemaType returns the JSON Schema type for the flag's value type. + func (f *FlagBase[T, C, V]) Set(_ string, val string) error Set applies given value from string @@ -1296,6 +1306,23 @@ type RequiredFlag interface { it allows flags required flags to be backwards compatible with the Flag interface +type SchemaItemsTyper interface { + // SchemaItemsType returns the JSON Schema type of elements for + // array-type flags. Returns "" for single-value or object flags. + SchemaItemsType() string +} + SchemaItemsTyper is an optional interface for multi-value flags that can + report the JSON Schema type of their elements. + +type SchemaTyper interface { + // SchemaType returns the JSON Schema type name for the value this + // flag accepts: "boolean", "integer", "number", "string", "array", + // "object". Returns "" if the flag does not map cleanly. + SchemaType() string +} + SchemaTyper is an optional interface for flags that can report their JSON + Schema type for programmatic introspection. + type Serializer interface { Serialize() string } diff --git a/testdata/godoc-v3.x.txt b/testdata/godoc-v3.x.txt index 681df19d63..ff52914972 100644 --- a/testdata/godoc-v3.x.txt +++ b/testdata/godoc-v3.x.txt @@ -405,6 +405,10 @@ func (bif *BoolWithInverseFlag) PreParse() error func (bif *BoolWithInverseFlag) RunAction(ctx context.Context, cmd *Command) error +func (bif *BoolWithInverseFlag) SchemaItemsType() string + +func (bif *BoolWithInverseFlag) SchemaType() string + func (bif *BoolWithInverseFlag) Set(name, val string) error func (bif *BoolWithInverseFlag) SetCategory(c string) @@ -1032,6 +1036,12 @@ func (f *FlagBase[T, C, V]) PreParse() error func (f *FlagBase[T, C, V]) RunAction(ctx context.Context, cmd *Command) error RunAction executes flag action if set +func (f *FlagBase[T, C, V]) SchemaItemsType() string + SchemaItemsType returns the JSON Schema element type for slice flags. + +func (f *FlagBase[T, C, V]) SchemaType() string + SchemaType returns the JSON Schema type for the flag's value type. + func (f *FlagBase[T, C, V]) Set(_ string, val string) error Set applies given value from string @@ -1296,6 +1306,23 @@ type RequiredFlag interface { it allows flags required flags to be backwards compatible with the Flag interface +type SchemaItemsTyper interface { + // SchemaItemsType returns the JSON Schema type of elements for + // array-type flags. Returns "" for single-value or object flags. + SchemaItemsType() string +} + SchemaItemsTyper is an optional interface for multi-value flags that can + report the JSON Schema type of their elements. + +type SchemaTyper interface { + // SchemaType returns the JSON Schema type name for the value this + // flag accepts: "boolean", "integer", "number", "string", "array", + // "object". Returns "" if the flag does not map cleanly. + SchemaType() string +} + SchemaTyper is an optional interface for flags that can report their JSON + Schema type for programmatic introspection. + type Serializer interface { Serialize() string }