Skip to content

Commit

Permalink
Merge pull request #50 from calyptia/add-section
Browse files Browse the repository at this point in the history
Move classic pkg to the root
  • Loading branch information
niedbalski authored Nov 24, 2022
2 parents ea014ba + e7b214a commit 3dfb7b0
Show file tree
Hide file tree
Showing 15 changed files with 508 additions and 527 deletions.
327 changes: 327 additions & 0 deletions classic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,327 @@
package fluentbitconfig

import (
"bytes"
"encoding"
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
"text/tabwriter"
"unicode/utf8"

"github.com/calyptia/go-fluentbit-config/property"
)

// reSpaces matches more than one consecutive spaces.
// This is used to split keys from values from the classic config.
var reSpaces = regexp.MustCompile(`\s+`)

func (c *Config) UnmarshalClassic(text []byte) error {
lines := strings.Split(string(text), "\n")

type Section struct {
Kind SectionKind
Props property.Properties
}

var currentSection *Section
flushCurrentSection := func() {
if currentSection == nil {
return
}

c.AddSection(currentSection.Kind, currentSection.Props)

currentSection = nil
}

for i, line := range lines {
lineNumber := uint(i + 1)
if !utf8.ValidString(line) {
return NewLinedError("invalid utf8 string", lineNumber)
}

line = strings.TrimSpace(line)

// ignore empty line
if line == "" {
continue
}

// ignore comment
if strings.HasPrefix(line, "#") {
continue
}

// command
if strings.HasPrefix(line, "@") {
flushCurrentSection()

line := strings.TrimPrefix(line, "@")
cmd, instruction, ok := spaceCut(line)
if !ok {
return NewLinedError("expected at least two strings separated by a space", lineNumber)
}

switch {
case strings.EqualFold(cmd, "INCLUDE"):
c.Include(instruction)
case strings.EqualFold(cmd, "SET"):
key, strValue, _ := strings.Cut(instruction, "=")
c.SetEnv(key, anyFromString(strValue))
}

continue
}

// beginning of section
if strings.HasPrefix(line, "[") {
flushCurrentSection()

if !strings.HasSuffix(line, "]") {
return NewLinedError(`expected section to end with "]"`, lineNumber)
}

sectionName := strings.Trim(line, "[]")
sectionName = strings.TrimSpace(sectionName)

if sectionName == "" {
return NewLinedError("expected section name to not be empty", lineNumber)
}

currentSection = &Section{
Kind: SectionKind(strings.ToLower(sectionName)),
Props: property.Properties{},
}

continue
}

if currentSection == nil {
return NewLinedError(fmt.Sprintf("unexpected entry %q", line), lineNumber)
}

key, strVal, ok := spaceCut(line)
if !ok {
return NewLinedError("expected at least two strings separated by a space", lineNumber)
}

value := anyFromString(strVal)

// Case when the property could be repeated. Example:
// [FILTER]
// Name record_modifier
// Match *
// Record hostname ${HOSTNAME}
// Record product Awesome_Tool
if v, ok := currentSection.Props.Get(key); ok {
if s, ok := v.([]any); ok {
s = append(s, value)
currentSection.Props.Set(key, s)
} else {
currentSection.Props.Set(key, []any{v, value})
}
} else {
currentSection.Props.Add(key, value)
}
}

flushCurrentSection()

return nil
}

func (c Config) MarshalClassic() ([]byte, error) {
var sb strings.Builder

for _, p := range c.Env {
_, err := fmt.Fprintf(&sb, "@SET %s=%s\n", p.Key, stringFromAny(p.Value))
if err != nil {
return nil, err
}
}

for _, include := range c.Includes {
_, err := fmt.Fprintf(&sb, "@INCLUDE %s\n", include)
if err != nil {
return nil, err
}
}

writeProps := func(kind string, props property.Properties) error {
if len(props) == 0 {
return nil
}

_, err := fmt.Fprintf(&sb, "[%s]\n", kind)
if err != nil {
return err
}

tw := tabwriter.NewWriter(&sb, 0, 4, 1, ' ', 0)
for _, p := range props {
if s, ok := p.Value.([]any); ok {
for _, v := range s {
_, err := fmt.Fprintf(tw, " %s\t%s\n", p.Key, stringFromAny(v))
if err != nil {
return err
}
}
} else {
_, err := fmt.Fprintf(tw, " %s\t%s\n", p.Key, stringFromAny(p.Value))
if err != nil {
return err
}
}
}
return tw.Flush()
}

writeByNames := func(kind string, byNames []ByName) error {
if len(byNames) == 0 || len(byNames[0]) == 0 {
return nil
}

for _, byName := range byNames {
for name, props := range byName {
if !props.Has("name") {
props.Set("name", name)
}

if err := writeProps(kind, props); err != nil {
return err
}
}
}

return nil
}

if err := writeProps("SERVICE", c.Service); err != nil {
return nil, err
}

if err := writeByNames("CUSTOM", c.Customs); err != nil {
return nil, err
}

if err := writeByNames("INPUT", c.Pipeline.Inputs); err != nil {
return nil, err
}

if err := writeByNames("PARSER", c.Pipeline.Parsers); err != nil {
return nil, err
}

if err := writeByNames("FILTER", c.Pipeline.Filters); err != nil {
return nil, err
}

if err := writeByNames("OUTPUT", c.Pipeline.Outputs); err != nil {
return nil, err
}

return []byte(sb.String()), nil
}

func spaceCut(s string) (before, after string, found bool) {
parts := reSpaces.Split(s, 2)
if len(parts) != 2 {
return "", "", false
}

return parts[0], parts[1], true
}

// anyFromString used to convert property values from the classic config
// to any type.
func anyFromString(s string) any {
// not using strconv since the boolean parser is not very strict
// and allows: 1, t, T, 0, f, F.
if strings.EqualFold(s, "true") {
return true
}

if strings.EqualFold(s, "false") {
return false
}

if v, err := strconv.ParseInt(s, 10, 64); err == nil {
return v
}

if v, err := strconv.ParseFloat(s, 64); err == nil {
return v
}

if u, err := strconv.Unquote(s); err == nil {
return u
}

return s
}

// stringFromAny -
// TODO: Handle more data types.
func stringFromAny(v any) string {
switch t := v.(type) {
case encoding.TextMarshaler:
if b, err := t.MarshalText(); err == nil {
return stringFromAny(string(b))
}
case fmt.Stringer:
return stringFromAny(t.String())
case json.Marshaler:
if b, err := t.MarshalJSON(); err == nil {
return stringFromAny(string(b))
}
case map[string]any:
var buff bytes.Buffer
enc := json.NewEncoder(&buff)
enc.SetEscapeHTML(false)
if err := enc.Encode(t); err == nil {
return buff.String()
}
case float32:
if isFloatInt(t) {
return strconv.FormatInt(int64(t), 10)
}
return fmtFloat(t)
case float64:
if isFloatInt(t) {
return strconv.FormatInt(int64(t), 10)
}
return fmtFloat(t)
case string:
if strings.Contains(t, "\n") {
return fmt.Sprintf("%q", t)
}

if t == "" {
return `""`
}

return t
}

return stringFromAny(fmt.Sprintf("%v", v))
}

func isFloatInt[F float32 | float64](f F) bool {
switch t := any(f).(type) {
case float32:
return t == float32(int32(f))
case float64:
return t == float64(int64(f))
}
return false
}

func fmtFloat[F float32 | float64](f F) string {
s := fmt.Sprintf("%f", f)
s = strings.TrimRight(s, "0")
s = strings.TrimRight(s, ".")
return s
}
55 changes: 0 additions & 55 deletions classic/classic.go

This file was deleted.

Loading

0 comments on commit 3dfb7b0

Please sign in to comment.