Skip to content
Merged
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
17 changes: 17 additions & 0 deletions flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions flag_bool_with_inverse.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
}
28 changes: 27 additions & 1 deletion flag_ext.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package cli

import "flag"
import (
"flag"
"time"
)

type extFlag struct {
f *flag.Flag
Expand Down Expand Up @@ -61,3 +64,26 @@ func (e *extFlag) GetDefaultText() string {
func (e *extFlag) GetEnvVars() []string {
return nil
}

func (e *extFlag) SchemaType() string {
switch e.Get().(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 "duration"
case time.Time:
return "date-time"
default:
return ""
}
}

func (e *extFlag) SchemaItemsType() string {
return ""
}
46 changes: 46 additions & 0 deletions flag_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"reflect"
"strings"
"time"
)

// Value represents a value as used by cli.
Expand Down Expand Up @@ -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 "duration"
case time.Time:
return "date-time"
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 {
Expand Down
235 changes: 235 additions & 0 deletions flag_schema_type_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
package cli

import (
"flag"
"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, "duration", st.SchemaType())
}

func TestFlag_SchemaType_Timestamp(t *testing.T) {
f := &TimestampFlag{}
st, ok := any(f).(SchemaTyper)
assert.True(t, ok)
assert.Equal(t, "date-time", 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 TestExtFlag_SchemaType(t *testing.T) {
tests := []struct {
name string
makeFlag func() *extFlag
schemaType string
}{
{
name: "bool",
makeFlag: func() *extFlag {
var bv boolValue
var p bool
return &extFlag{f: &flag.Flag{Value: bv.Create(true, &p, BoolConfig{})}}
},
schemaType: "boolean",
},
{
name: "string",
makeFlag: func() *extFlag {
var sv stringValue
var p string
return &extFlag{f: &flag.Flag{Value: sv.Create("hi", &p, StringConfig{})}}
},
schemaType: "string",
},
{
name: "float64",
makeFlag: func() *extFlag {
var fv floatValue[float64]
var p float64
return &extFlag{f: &flag.Flag{Value: fv.Create(3.14, &p, NoConfig{})}}
},
schemaType: "number",
},
{
name: "duration",
makeFlag: func() *extFlag {
var dv durationValue
var p time.Duration
return &extFlag{f: &flag.Flag{Value: dv.Create(5*time.Second, &p, NoConfig{})}}
},
schemaType: "duration",
},
{
name: "timestamp",
makeFlag: func() *extFlag {
var tv timestampValue
var p time.Time
return &extFlag{f: &flag.Flag{Value: tv.Create(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), &p, TimestampConfig{})}}
},
schemaType: "date-time",
},
{
name: "unknown",
makeFlag: func() *extFlag {
return &extFlag{f: &flag.Flag{Value: &customValue{}}}
},
schemaType: "",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.schemaType, tc.makeFlag().SchemaType())
assert.Equal(t, "", tc.makeFlag().SchemaItemsType())
})
}
}

type customValue struct{}

func (c *customValue) String() string { return "custom" }
func (c *customValue) Set(string) error { return nil }
func (c *customValue) Get() any { return struct{}{} }

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, "date-time", f.SchemaType())

f2 := &DurationFlag{Value: 5 * time.Second}
assert.Equal(t, "duration", f2.SchemaType())
}
3 changes: 3 additions & 0 deletions flag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3328,6 +3328,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, "integer", extF.SchemaType())
assert.Equal(t, "", extF.SchemaItemsType())
}

func TestSliceValuesNil(t *testing.T) {
Expand Down
Loading
Loading