Comprehensive guide on implementing new field types in the Glazed framework.
Field types in glazed are defined in the glazed/pkg/cmds/fields package. Each field type requires modifications to several files to handle:
This guide explains how to add a new field type to the glazed command line framework. We'll use the example of adding a credentials field type to demonstrate the process.
When adding a new field type, you need to modify these files:
field-type.go - Define the type constant and metadata methodsparse.go - Add parsing logicfields.go - Add validation and value assignmentcobra.go - Add CLI flag supportviper.go - Add configuration file supportrender.go - Add display formattingjson-schema.go - Add JSON schema type mappingglazed.go - Add Go type mapping for code generationlua.go - Add Lua value parsing supportNote: The linter will help you find any additional files with exhaustive switch statements that need updating by running make lint or golangci-lint run.
In field-type.go, add your new type constant:
const (
// ... existing types ...
TypeCredentials Type = "credentials"
)
Add your type to the relevant metadata methods. For example, if credentials should be treated as a list:
func (p Type) IsList() bool {
switch p {
case TypeCredentials:
return true
// ... existing cases ...
}
}
Add other metadata methods as needed:
NeedsFileContent() - if type can load from filesNeedsMultipleFileContent() - if type loads from multiple filesIsFile() - if type represents file dataIsObject() - if type represents structured objectsIsKeyValue() - if type represents key-value mapsIn parse.go, add a case to the ParseField method:
func (p *Definition) ParseField(v []string, options ...ParseStepOption) (*FieldValue, error) {
// ... existing code ...
switch p.Type {
// ... existing cases ...
case TypeCredentials:
// Parse credentials from command line arguments
// Example: expect format "username:password" or load from file with @
if len(v) == 1 && strings.HasPrefix(v[0], "@") {
// Load from file
credFile := v[0][1:]
content, err := os.ReadFile(credFile)
if err != nil {
return nil, errors.Wrapf(err, "Could not read credentials file %s", credFile)
}
// Parse JSON/YAML credentials file
var creds map[string]string
if strings.HasSuffix(credFile, ".json") {
err = json.Unmarshal(content, &creds)
} else {
err = yaml.Unmarshal(content, &creds)
}
if err != nil {
return nil, errors.Wrapf(err, "Could not parse credentials file %s", credFile)
}
v_ = creds
} else {
// Parse from command line
creds := make(map[string]string)
for _, arg := range v {
parts := strings.SplitN(arg, ":", 2)
if len(parts) != 2 {
return nil, errors.Errorf("Invalid credentials format: %s (expected username:password)", arg)
}
creds[parts[0]] = parts[1]
}
v_ = creds
}
}
// ... rest of method ...
}
If your type supports file loading, add cases to ParseFromReader:
func (p *Definition) ParseFromReader(f io.Reader, filename string, options ...ParseStepOption) (*FieldValue, error) {
// ... existing code ...
switch p.Type {
// ... existing cases ...
case TypeCredentials:
var creds map[string]string
if strings.HasSuffix(filename, ".json") {
err = json.NewDecoder(f).Decode(&creds)
} else {
err = yaml.NewDecoder(f).Decode(&creds)
}
if err != nil {
return nil, err
}
err = ret.Update(creds, options...)
return ret, err
}
}
In fields.go, add validation to CheckValueValidity:
func (p *Definition) CheckValueValidity(v interface{}) (interface{}, error) {
// ... existing code ...
switch p.Type {
// ... existing cases ...
case TypeCredentials:
creds, ok := v.(map[string]string)
if !ok {
// Try to convert from map[string]interface{}
credsIface, ok := v.(map[string]interface{})
if !ok {
return nil, errors.Errorf("Value for field %s is not credentials (expected map[string]string): %v", p.Name, v)
}
creds = make(map[string]string)
for k, v := range credsIface {
str, ok := v.(string)
if !ok {
return nil, errors.Errorf("Credentials value for key %s is not a string: %v", k, v)
}
creds[k] = str
}
}
// Validate required fields
if _, ok := creds["username"]; !ok {
return nil, errors.Errorf("Credentials missing required 'username' field")
}
if _, ok := creds["password"]; !ok {
return nil, errors.Errorf("Credentials missing required 'password' field")
}
return creds, nil
}
}
Add empty value initialization to InitializeValueToEmptyValue:
func (p *Definition) InitializeValueToEmptyValue(value reflect.Value) error {
switch p.Type {
// ... existing cases ...
case TypeCredentials:
value.Set(reflect.ValueOf(map[string]string{}))
}
}
Add value assignment to SetValueFromInterface:
func (p *Definition) SetValueFromInterface(value reflect.Value, v interface{}) error {
// ... validation ...
switch p.Type {
// ... existing cases ...
case TypeCredentials:
creds, ok := v.(map[string]string)
if !ok {
return errors.Errorf("expected credentials for field %s, got %T", p.Name, v)
}
value.Set(reflect.ValueOf(creds))
}
}
In cobra.go, add flag creation logic:
func (ps *Definitions) AddToCobraCommand(cmd *cobra.Command) error {
// ... existing code ...
switch field.Type {
// ... existing cases ...
case TypeCredentials:
defaultValue := []string{}
if field.Default != nil {
if creds, ok := (*field.Default).(map[string]string); ok {
for k, v := range creds {
defaultValue = append(defaultValue, k+":"+v)
}
}
}
cmd.Flags().StringSliceVarP(&ps.cobraFieldValues[field.Name],
field.Name, field.ShortFlag, defaultValue, field.Help)
}
}
Add completion logic if needed:
func (ps *Definitions) SetupCobraCompletions(cmd *cobra.Command) error {
// ... existing code ...
switch field.Type {
case TypeCredentials:
err = cmd.RegisterFlagCompletionFunc(field.Name, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"username:", "token:", "api_key:"}, cobra.ShellCompDirectiveNoSpace
})
}
}
In render.go, add display formatting:
func RenderValue(fieldType Type, value interface{}) (string, error) {
switch fieldType {
// ... existing cases ...
case TypeCredentials:
if creds, ok := value.(map[string]string); ok {
// Mask sensitive data for display
rendered := make([]string, 0, len(creds))
for k, v := range creds {
if strings.Contains(strings.ToLower(k), "password") ||
strings.Contains(strings.ToLower(k), "secret") ||
strings.Contains(strings.ToLower(k), "token") {
rendered = append(rendered, k+":***")
} else {
rendered = append(rendered, k+":"+v)
}
}
return strings.Join(rendered, ", "), nil
}
return "", errors.Errorf("Invalid credentials value: %v", value)
}
}
After implementing the core field functionality, you may need to update additional files that have exhaustive switch statements on field types:
json-schema.go)switch field.Type {
// ... existing cases ...
case fields.TypeCredentials:
prop.Type = "object"
prop.Properties = map[string]*JSONSchemaProperty{
"username": {Type: "string"},
"password": {Type: "string"},
}
}
codegen/glazed.go)func FlagTypeToGoType(s *jen.Statement, fieldType fields.Type) *jen.Statement {
switch fieldType {
// ... existing cases ...
case fields.TypeCredentials:
return s.Map(jen.Id("string")).Id("string")
}
}
lua/lua.go)func ParseFieldFromLua(L *lua.LState, value lua.LValue, fieldDef *fields.Definition) (interface{}, error) {
switch fieldDef.Type {
// ... existing cases ...
case fields.TypeCredentials:
if table, ok := value.(*lua.LTable); ok {
creds := make(map[string]string)
table.ForEach(func(k, v lua.LValue) {
if keyStr, ok := k.(lua.LString); ok {
if valStr, ok := v.(lua.LString); ok {
creds[string(keyStr)] = string(valStr)
}
}
})
return creds, nil
}
return nil, fmt.Errorf("invalid type for credentials field '%s': expected table, got %s", fieldDef.Name, value.Type())
}
}
Pro Tip: Run make lint or golangci-lint run after adding your field type to discover any additional files with exhaustive switches that need updating.
Update the field types example command in cmd/examples/field-types/main.go to showcase your new field type.
Add your field to the cmds.WithFlags() section:
fields.New(
"credentials-field",
fields.TypeCredentials,
fields.WithHelp("A credentials field for username/password pairs"),
fields.WithDefault(map[string]string{"username": "admin", "password": "secret"}),
),
Add a field to the TypesSettings struct:
type TypesSettings struct {
// ... existing fields ...
CredentialsField map[string]string `glazed:"credentials-field"`
}
Add an entry to the fieldData slice in RunIntoGlazeProcessor:
{"credentials-field", fields.TypeCredentials, s.CredentialsField, "A credentials field for username/password pairs", false, nil, map[string]string{"username": "admin", "password": "secret"}},
This ensures that developers and users can easily test and understand how your new field type works in practice.
After updating the example, test it to ensure your new field type works correctly:
cd cmd/examples/field-types
go build -o field-types .
# Test with default values
./field-types field-types
# Test with custom values for your new type
./field-types field-types --credentials-field username:admin,password:secret
# Test field parsing (useful for debugging)
./field-types field-types --credentials-field username:test,password:demo --print-parsed-fields
Verify that:
--help)Create comprehensive tests for your new field type:
func TestCredentialsField(t *testing.T) {
tests := []struct {
name string
input []string
expected map[string]string
wantErr bool
}{
{
name: "single credential",
input: []string{"user:pass"},
expected: map[string]string{"user": "pass"},
},
{
name: "multiple credentials",
input: []string{"user:pass", "api_key:secret"},
expected: map[string]string{"user": "pass", "api_key": "secret"},
},
{
name: "invalid format",
input: []string{"invalid"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pd := &Definition{
Name: "credentials",
Type: TypeCredentials,
}
result, err := pd.ParseField(tt.input)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.expected, result.Value)
})
}
}
Here's what the complete implementation would look like for a credentials field type:
const (
// ... existing types ...
TypeCredentials Type = "credentials"
)
func (p Type) IsKeyValue() bool {
switch p {
case TypeKeyValue, TypeCredentials:
return true
default:
return false
}
}
This implementation would:
username:password format from command line@filename syntaxWhen adding a new field type to glazed, you need to modify these core files and follow these steps:
field-type.goparse.gofields.gocobra.goviper.gorender.gocmd/examples/field-types/main.goType<Name> for constantsFollow these patterns when implementing your custom field type to ensure consistency with the rest of the glazed framework.
Important: The field types example in cmd/examples/field-types/ serves as both documentation and a testing tool. Always update it when adding new field types so users and developers can easily understand and test the new functionality.