448 lines
11 KiB
Go
448 lines
11 KiB
Go
// Package paramsparser provides functionality for parsing and manipulating parameters
|
|
// from text in a key-value format with support for multiline strings.
|
|
package paramsparser
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/freeflowuniverse/heroagent/pkg/tools"
|
|
)
|
|
|
|
// ParamsParser represents a parameter parser that can handle various parameter sources
|
|
type ParamsParser struct {
|
|
params map[string]string
|
|
defaultParams map[string]string
|
|
}
|
|
|
|
// New creates a new ParamsParser instance
|
|
func New() *ParamsParser {
|
|
return &ParamsParser{
|
|
params: make(map[string]string),
|
|
defaultParams: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
// Parse parses a string containing key-value pairs in the format:
|
|
// key:value or key:'value'
|
|
// It supports multiline string values.
|
|
func (p *ParamsParser) Parse(input string) error {
|
|
// Normalize line endings
|
|
input = strings.ReplaceAll(input, "\r\n", "\n")
|
|
|
|
// Track the current state
|
|
var currentKey string
|
|
var currentValue strings.Builder
|
|
var inMultilineString bool
|
|
|
|
// Process each line
|
|
lines := strings.Split(input, "\n")
|
|
for i := 0; i < len(lines); i++ {
|
|
// Only trim space for non-multiline string processing
|
|
var line string
|
|
if !inMultilineString {
|
|
line = strings.TrimSpace(lines[i])
|
|
} else {
|
|
line = lines[i]
|
|
}
|
|
|
|
// Skip empty lines unless we're in a multiline string
|
|
if line == "" && !inMultilineString {
|
|
continue
|
|
}
|
|
|
|
// If we're in a multiline string
|
|
if inMultilineString {
|
|
// Check if this line ends the multiline string
|
|
if strings.HasSuffix(line, "'") && !strings.HasSuffix(line, "\\'") {
|
|
// Add the line without the closing quote
|
|
currentValue.WriteString(line[:len(line)-1])
|
|
p.params[currentKey] = currentValue.String()
|
|
inMultilineString = false
|
|
currentKey = ""
|
|
currentValue.Reset()
|
|
} else {
|
|
// Continue the multiline string
|
|
currentValue.WriteString(line)
|
|
currentValue.WriteString("\n")
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Process the line to extract key-value pairs
|
|
var processedPos int
|
|
for processedPos < len(line) {
|
|
// Skip leading whitespace
|
|
for processedPos < len(line) && (line[processedPos] == ' ' || line[processedPos] == '\t') {
|
|
processedPos++
|
|
}
|
|
|
|
if processedPos >= len(line) {
|
|
break
|
|
}
|
|
|
|
// Find the next key by looking for a colon
|
|
keyStart := processedPos
|
|
colonPos := -1
|
|
|
|
for j := processedPos; j < len(line); j++ {
|
|
if line[j] == ':' {
|
|
colonPos = j
|
|
break
|
|
}
|
|
}
|
|
|
|
if colonPos == -1 {
|
|
// No colon found, skip this part
|
|
break
|
|
}
|
|
|
|
// Extract key and use NameFix to standardize it
|
|
rawKey := strings.TrimSpace(line[keyStart:colonPos])
|
|
key := tools.NameFix(rawKey)
|
|
|
|
if key == "" {
|
|
// Invalid key, move past the colon and continue
|
|
processedPos = colonPos + 1
|
|
continue
|
|
}
|
|
|
|
// Move position past the colon
|
|
processedPos = colonPos + 1
|
|
|
|
if processedPos >= len(line) {
|
|
// End of line reached, store empty value
|
|
p.params[key] = ""
|
|
break
|
|
}
|
|
|
|
// Skip whitespace after the colon
|
|
for processedPos < len(line) && (line[processedPos] == ' ' || line[processedPos] == '\t') {
|
|
processedPos++
|
|
}
|
|
|
|
if processedPos >= len(line) {
|
|
// End of line reached after whitespace, store empty value
|
|
p.params[key] = ""
|
|
break
|
|
}
|
|
|
|
// Check if the value is quoted
|
|
if line[processedPos] == '\'' {
|
|
// This is a quoted string
|
|
processedPos++ // Skip the opening quote
|
|
|
|
// Look for the closing quote
|
|
quoteEnd := -1
|
|
for j := processedPos; j < len(line); j++ {
|
|
// Check for escaped quote
|
|
if line[j] == '\'' && (j == 0 || line[j-1] != '\\') {
|
|
quoteEnd = j
|
|
break
|
|
}
|
|
}
|
|
|
|
if quoteEnd != -1 {
|
|
// Single-line quoted string
|
|
value := line[processedPos:quoteEnd]
|
|
// For quoted values, we preserve the original formatting
|
|
// But for single-line values, we can apply NameFix if needed
|
|
if key != "description" {
|
|
value = tools.NameFix(value)
|
|
}
|
|
p.params[key] = value
|
|
processedPos = quoteEnd + 1 // Move past the closing quote
|
|
} else {
|
|
// Start of multiline string
|
|
currentKey = key
|
|
currentValue.WriteString(line[processedPos:])
|
|
currentValue.WriteString("\n")
|
|
inMultilineString = true
|
|
break
|
|
}
|
|
} else {
|
|
// This is an unquoted value
|
|
valueStart := processedPos
|
|
valueEnd := valueStart
|
|
|
|
// Find the end of the value (space or end of line)
|
|
for valueEnd < len(line) && line[valueEnd] != ' ' && line[valueEnd] != '\t' {
|
|
valueEnd++
|
|
}
|
|
|
|
value := line[valueStart:valueEnd]
|
|
// For unquoted values, use NameFix to standardize them
|
|
// This handles the 'without' keyword and other special cases
|
|
p.params[key] = tools.NameFix(value)
|
|
processedPos = valueEnd
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we're still in a multiline string at the end, that's an error
|
|
if inMultilineString {
|
|
return errors.New("unterminated multiline string")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ParseString is a simpler version that parses a string with the format:
|
|
// key:value or key:'value'
|
|
// This version doesn't support multiline strings and is optimized for one-line inputs
|
|
func (p *ParamsParser) ParseString(input string) error {
|
|
// Trim the input
|
|
input = strings.TrimSpace(input)
|
|
|
|
// Process the input to extract key-value pairs
|
|
var processedPos int
|
|
for processedPos < len(input) {
|
|
// Skip leading whitespace
|
|
for processedPos < len(input) && (input[processedPos] == ' ' || input[processedPos] == '\t') {
|
|
processedPos++
|
|
}
|
|
|
|
if processedPos >= len(input) {
|
|
break
|
|
}
|
|
|
|
// Find the next key by looking for a colon
|
|
keyStart := processedPos
|
|
colonPos := -1
|
|
|
|
for j := processedPos; j < len(input); j++ {
|
|
if input[j] == ':' {
|
|
colonPos = j
|
|
break
|
|
}
|
|
}
|
|
|
|
if colonPos == -1 {
|
|
// No colon found, skip this part
|
|
break
|
|
}
|
|
|
|
// Extract key and use NameFix to standardize it
|
|
rawKey := strings.TrimSpace(input[keyStart:colonPos])
|
|
key := tools.NameFix(rawKey)
|
|
|
|
if key == "" {
|
|
// Invalid key, move past the colon and continue
|
|
processedPos = colonPos + 1
|
|
continue
|
|
}
|
|
|
|
// Move position past the colon
|
|
processedPos = colonPos + 1
|
|
|
|
if processedPos >= len(input) {
|
|
// End of input reached, store empty value
|
|
p.params[key] = ""
|
|
break
|
|
}
|
|
|
|
// Skip whitespace after the colon
|
|
for processedPos < len(input) && (input[processedPos] == ' ' || input[processedPos] == '\t') {
|
|
processedPos++
|
|
}
|
|
|
|
if processedPos >= len(input) {
|
|
// End of input reached after whitespace, store empty value
|
|
p.params[key] = ""
|
|
break
|
|
}
|
|
|
|
// Check if the value is quoted
|
|
if input[processedPos] == '\'' {
|
|
// This is a quoted string
|
|
processedPos++ // Skip the opening quote
|
|
|
|
// Look for the closing quote
|
|
quoteEnd := -1
|
|
for j := processedPos; j < len(input); j++ {
|
|
// Check for escaped quote
|
|
if input[j] == '\'' && (j == 0 || input[j-1] != '\\') {
|
|
quoteEnd = j
|
|
break
|
|
}
|
|
}
|
|
|
|
if quoteEnd == -1 {
|
|
return errors.New("unterminated quoted string")
|
|
}
|
|
|
|
value := input[processedPos:quoteEnd]
|
|
// For quoted values in ParseString, we can apply NameFix
|
|
// since this method doesn't handle multiline strings
|
|
if key != "description" {
|
|
value = tools.NameFix(value)
|
|
}
|
|
p.params[key] = value
|
|
processedPos = quoteEnd + 1 // Move past the closing quote
|
|
} else {
|
|
// This is an unquoted value
|
|
valueStart := processedPos
|
|
valueEnd := valueStart
|
|
|
|
// Find the end of the value (space or end of input)
|
|
for valueEnd < len(input) && input[valueEnd] != ' ' && input[valueEnd] != '\t' {
|
|
valueEnd++
|
|
}
|
|
|
|
value := input[valueStart:valueEnd]
|
|
// For unquoted values, use NameFix to standardize them
|
|
// This handles the 'without' keyword and other special cases
|
|
p.params[key] = tools.NameFix(value)
|
|
processedPos = valueEnd
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ParseFile parses a file containing key-value pairs
|
|
func (p *ParamsParser) ParseFile(filename string) error {
|
|
data, err := os.ReadFile(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return p.Parse(string(data))
|
|
}
|
|
|
|
// SetDefault sets a default value for a parameter
|
|
func (p *ParamsParser) SetDefault(key, value string) {
|
|
p.defaultParams[key] = value
|
|
}
|
|
|
|
// SetDefaults sets multiple default values at once
|
|
func (p *ParamsParser) SetDefaults(defaults map[string]string) {
|
|
for k, v := range defaults {
|
|
p.defaultParams[k] = v
|
|
}
|
|
}
|
|
|
|
// Set explicitly sets a parameter value
|
|
func (p *ParamsParser) Set(key, value string) {
|
|
p.params[key] = value
|
|
}
|
|
|
|
// Get retrieves a parameter value, returning the default if not found
|
|
func (p *ParamsParser) Get(key string) string {
|
|
if value, exists := p.params[key]; exists {
|
|
return value
|
|
}
|
|
if defaultValue, exists := p.defaultParams[key]; exists {
|
|
return defaultValue
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// GetInt retrieves a parameter as an integer
|
|
func (p *ParamsParser) GetInt(key string) (int, error) {
|
|
value := p.Get(key)
|
|
if value == "" {
|
|
return 0, errors.New("parameter not found")
|
|
}
|
|
return strconv.Atoi(value)
|
|
}
|
|
|
|
// GetIntDefault retrieves a parameter as an integer with a default value
|
|
func (p *ParamsParser) GetIntDefault(key string, defaultValue int) int {
|
|
value, err := p.GetInt(key)
|
|
if err != nil {
|
|
return defaultValue
|
|
}
|
|
return value
|
|
}
|
|
|
|
// GetBool retrieves a parameter as a boolean
|
|
func (p *ParamsParser) GetBool(key string) bool {
|
|
value := p.Get(key)
|
|
if value == "" {
|
|
return false
|
|
}
|
|
|
|
// Check for common boolean string representations
|
|
value = strings.ToLower(value)
|
|
return value == "true" || value == "yes" || value == "1" || value == "on"
|
|
}
|
|
|
|
// GetBoolDefault retrieves a parameter as a boolean with a default value
|
|
func (p *ParamsParser) GetBoolDefault(key string, defaultValue bool) bool {
|
|
if !p.Has(key) {
|
|
return defaultValue
|
|
}
|
|
return p.GetBool(key)
|
|
}
|
|
|
|
// GetFloat retrieves a parameter as a float64
|
|
func (p *ParamsParser) GetFloat(key string) (float64, error) {
|
|
value := p.Get(key)
|
|
if value == "" {
|
|
return 0, errors.New("parameter not found")
|
|
}
|
|
return strconv.ParseFloat(value, 64)
|
|
}
|
|
|
|
// GetFloatDefault retrieves a parameter as a float64 with a default value
|
|
func (p *ParamsParser) GetFloatDefault(key string, defaultValue float64) float64 {
|
|
value, err := p.GetFloat(key)
|
|
if err != nil {
|
|
return defaultValue
|
|
}
|
|
return value
|
|
}
|
|
|
|
// Has checks if a parameter exists
|
|
func (p *ParamsParser) Has(key string) bool {
|
|
_, exists := p.params[key]
|
|
return exists
|
|
}
|
|
|
|
// GetAll returns all parameters as a map
|
|
func (p *ParamsParser) GetAll() map[string]string {
|
|
result := make(map[string]string)
|
|
|
|
// First add defaults
|
|
for k, v := range p.defaultParams {
|
|
result[k] = v
|
|
}
|
|
|
|
// Then override with actual params
|
|
for k, v := range p.params {
|
|
result[k] = v
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// MustGet retrieves a parameter value, panicking if not found
|
|
func (p *ParamsParser) MustGet(key string) string {
|
|
value := p.Get(key)
|
|
if value == "" {
|
|
panic(fmt.Sprintf("required parameter '%s' not found", key))
|
|
}
|
|
return value
|
|
}
|
|
|
|
// MustGetInt retrieves a parameter as an integer, panicking if not found or invalid
|
|
func (p *ParamsParser) MustGetInt(key string) int {
|
|
value, err := p.GetInt(key)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("required integer parameter '%s' not found or invalid", key))
|
|
}
|
|
return value
|
|
}
|
|
|
|
// MustGetFloat retrieves a parameter as a float64, panicking if not found or invalid
|
|
func (p *ParamsParser) MustGetFloat(key string) float64 {
|
|
value, err := p.GetFloat(key)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("required float parameter '%s' not found or invalid", key))
|
|
}
|
|
return value
|
|
}
|