// Package template contains the logic for parsing template blocks. package template import ( "fmt" "strings" "go.yaml.in/yaml/v4" ) // TemplateType represents whether a template is short, long, or invalid. type TemplateType int const ( InvalidTemplate TemplateType = iota ShortTemplate ExtendedTemplate ) // DetectTemplateType checks if the template is short or long. func DetectTemplateType(tmpl string) TemplateType { trimmed := strings.TrimSpace(tmpl) // Short type: starts with "{" and ends with "}" on a single line, // and contains "|" or "," inside for inline definition // Matchs for example { name | text,required } if strings.HasPrefix(trimmed, "{") && strings.HasSuffix(trimmed, "}") && !strings.Contains(trimmed, "\n") { return ShortTemplate } // Long type: multiline and contains keys like "path:" or "codec:" inside // Matches for example: // { // path: name // codec: text // required: true // } if strings.Contains(trimmed, "\n") && (strings.Contains(trimmed, "path:") || strings.Contains(trimmed, "codec:")) { return ExtendedTemplate } return InvalidTemplate } func cleanTemplate(input string) string { s := strings.TrimSpace(input) s = strings.TrimPrefix(s, "{") s = strings.TrimSuffix(s, "}") s = strings.Trim(s, "\n") return s } func parseShortTemplate(input string) (Block, error) { split := strings.Split(cleanTemplate(input), "|") if len(split) < 1 { return Block{}, fmt.Errorf("invalid short template") } block := Block{ Type: DataBlock, Path: strings.TrimSpace(split[0]), Codec: CodecText, content: input, } if len(split) > 1 { optionSplit := strings.SplitSeq(split[1], ",") for option := range optionSplit { switch strings.TrimSpace(option) { case "required": block.Required = true case "number": block.Codec = CodecNumber } } } return block, nil } type yamlBlock struct { Path string `yaml:"path"` Codec string `yaml:"codec"` Required bool `yaml:"required,omitempty"` Value any `yaml:"value,omitempty"` Fields []yamlField `yaml:"fields"` ListTemplate string `yaml:"listTemplate,omitempty"` } type yamlField struct { Path string `yaml:"path"` Value any `yaml:"value,omitempty"` Codec string `yaml:"codec"` Required bool `yaml:"required"` } func parseYamlTemplate(input string) (block Block, err error) { var blk yamlBlock cleaned := cleanTemplate(input) dec := yaml.NewDecoder(strings.NewReader(cleaned)) dec.KnownFields(true) if err := dec.Decode(&blk); err != nil { return block, fmt.Errorf("content '%q': %w", cleaned, err) } if blk.Path == "" { return block, fmt.Errorf("missing top-level 'path'") } if blk.Codec == "" { blk.Codec = "text" } codec, err := parseCodecType(blk.Codec) if err != nil { return block, fmt.Errorf("failed to parse codec: %w", err) } var fields []BlockField for _, field := range blk.Fields { if field.Path == "" { return block, fmt.Errorf("failed to parse field: %v", field) } if field.Codec == "" { field.Codec = "text" } fieldCodec, err := parseCodecType(field.Codec) if err != nil { return block, fmt.Errorf("failed to parse codec: %w", err) } fields = append(fields, BlockField{ Path: field.Path, CodecType: fieldCodec, Required: field.Required, Value: field.Value, }) } return Block{ Type: DataBlock, Path: blk.Path, Codec: codec, Fields: fields, ListTemplate: blk.ListTemplate, content: input, }, nil } func ParseTemplateBlock(template string, blockType BlockType) (block Block, err error) { if blockType == MatchingBlock { return Block{ Type: MatchingBlock, content: template, }, nil } switch DetectTemplateType(template) { case ShortTemplate: return parseShortTemplate(template) case ExtendedTemplate: return parseYamlTemplate(template) } return block, fmt.Errorf("invalid template") }