297 lines
		
	
	
	
		
			5.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			297 lines
		
	
	
	
		
			5.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package models
 | |
| 
 | |
| import (
 | |
| 	"bufio"
 | |
| 	"bytes"
 | |
| 	"encoding/json"
 | |
| 	"errors"
 | |
| 	"regexp"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"codeberg.org/danjones000/my-log/tools"
 | |
| )
 | |
| 
 | |
| const DateFormat = tools.DateFormat
 | |
| 
 | |
| type Entry struct {
 | |
| 	Title       string
 | |
| 	Date        time.Time
 | |
| 	Fields      []Meta
 | |
| 	skipMissing bool
 | |
| }
 | |
| 
 | |
| func PartialEntry() Entry {
 | |
| 	return Entry{skipMissing: true}
 | |
| }
 | |
| 
 | |
| type metaRes struct {
 | |
| 	out []byte
 | |
| 	err error
 | |
| }
 | |
| 
 | |
| func (e Entry) getFieldMarshalChan() chan metaRes {
 | |
| 	size := len(e.Fields)
 | |
| 	ch := make(chan metaRes, size)
 | |
| 	var wg sync.WaitGroup
 | |
| 
 | |
| 	for i := 0; i < size; i++ {
 | |
| 		wg.Add(1)
 | |
| 		go func(m Meta) {
 | |
| 			defer wg.Done()
 | |
| 			if m.Key == "json" {
 | |
| 				if j, ok := m.Value.(json.RawMessage); ok {
 | |
| 					sub := Entry{skipMissing: true}
 | |
| 					json.Unmarshal(j, &sub)
 | |
| 					for _, subM := range sub.Fields {
 | |
| 						o, er := subM.MarshalText()
 | |
| 						ch <- metaRes{o, er}
 | |
| 					}
 | |
| 				}
 | |
| 			} else {
 | |
| 				o, er := m.MarshalText()
 | |
| 				ch <- metaRes{o, er}
 | |
| 			}
 | |
| 
 | |
| 		}(e.Fields[i])
 | |
| 	}
 | |
| 
 | |
| 	go func() {
 | |
| 		wg.Wait()
 | |
| 		close(ch)
 | |
| 	}()
 | |
| 	return ch
 | |
| }
 | |
| 
 | |
| func (e Entry) MarshalText() ([]byte, error) {
 | |
| 	e.Title = strings.TrimSpace(e.Title)
 | |
| 	if e.Title == "" {
 | |
| 		return []byte{}, ErrorMissingTitle
 | |
| 	}
 | |
| 	if e.Date == (time.Time{}) {
 | |
| 		return []byte{}, ErrorMissingDate
 | |
| 	}
 | |
| 	ch := e.getFieldMarshalChan()
 | |
| 	buff := &bytes.Buffer{}
 | |
| 	buff.WriteString("@begin ")
 | |
| 	buff.WriteString(e.Date.Format(DateFormat))
 | |
| 	buff.WriteString(" - ")
 | |
| 	buff.WriteString(e.Title)
 | |
| 
 | |
| 	for res := range ch {
 | |
| 		if res.err == nil && len(res.out) > 0 {
 | |
| 			buff.WriteString("\n")
 | |
| 			buff.Write(res.out)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	buff.WriteString(" @end")
 | |
| 
 | |
| 	return buff.Bytes(), nil
 | |
| }
 | |
| 
 | |
| func (m *Entry) UnmarshalText(in []byte) error {
 | |
| 	re := regexp.MustCompile("(?s)^@begin (.+) - (.+?)[ \n]@")
 | |
| 	match := re.FindSubmatch(in)
 | |
| 	if len(match) == 0 {
 | |
| 		return newParsingError(errors.New("Failed to find title and date"))
 | |
| 	}
 | |
| 
 | |
| 	ch := m.getFieldUnarshalChan(in)
 | |
| 
 | |
| 	title := bytes.TrimSpace(match[2])
 | |
| 	if len(title) == 0 {
 | |
| 		return ErrorMissingTitle
 | |
| 	}
 | |
| 	m.Title = string(title)
 | |
| 	date := string(bytes.TrimSpace(match[1]))
 | |
| 	if date == "" {
 | |
| 		return ErrorMissingDate
 | |
| 	}
 | |
| 	d, e := tools.ParseDate(date)
 | |
| 	if e != nil {
 | |
| 		return newParsingError(e)
 | |
| 	}
 | |
| 	m.Date = d
 | |
| 
 | |
| 	for meta := range ch {
 | |
| 		m.Fields = append(m.Fields, meta)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func scanEntry(data []byte, atEOF bool) (advance int, token []byte, err error) {
 | |
| 	if atEOF && len(data) == 0 {
 | |
| 		return 0, nil, nil
 | |
| 	}
 | |
| 	if i := bytes.Index(data, []byte{10, 64}); i > 0 {
 | |
| 		return i + 1, data[0:i], nil
 | |
| 	}
 | |
| 	if atEOF {
 | |
| 		end := []byte{32, 64, 101, 110, 100}
 | |
| 		token = data
 | |
| 		if i := bytes.Index(data, end); i >= 0 {
 | |
| 			token = data[0:i]
 | |
| 		}
 | |
| 		return len(data), token, nil
 | |
| 	}
 | |
| 	// Request more data.
 | |
| 	return 0, nil, nil
 | |
| }
 | |
| 
 | |
| func (e *Entry) getFieldUnarshalChan(in []byte) chan Meta {
 | |
| 	size := len(in) / 3 // rough estimation
 | |
| 	ch := make(chan Meta, size)
 | |
| 	var wg sync.WaitGroup
 | |
| 
 | |
| 	read := bytes.NewReader(in)
 | |
| 	scan := bufio.NewScanner(read)
 | |
| 	scan.Split(scanEntry)
 | |
| 	scan.Scan() // throw out first line
 | |
| 
 | |
| 	for scan.Scan() {
 | |
| 		wg.Add(1)
 | |
| 		go func(field []byte) {
 | |
| 			defer wg.Done()
 | |
| 			m := new(Meta)
 | |
| 			err := m.UnmarshalText(field)
 | |
| 			if err == nil {
 | |
| 				if m.Key == "json" {
 | |
| 					if j, ok := m.Value.(json.RawMessage); ok {
 | |
| 						sub := Entry{skipMissing: true}
 | |
| 						json.Unmarshal(j, &sub)
 | |
| 						for _, subM := range sub.Fields {
 | |
| 							ch <- subM
 | |
| 						}
 | |
| 					}
 | |
| 				} else {
 | |
| 					ch <- *m
 | |
| 				}
 | |
| 			}
 | |
| 		}(scan.Bytes())
 | |
| 	}
 | |
| 
 | |
| 	go func() {
 | |
| 		wg.Wait()
 | |
| 		close(ch)
 | |
| 	}()
 | |
| 	return ch
 | |
| }
 | |
| 
 | |
| func (e Entry) MarshalJSON() ([]byte, error) {
 | |
| 	if e.Title == "" {
 | |
| 		return []byte{}, ErrorMissingTitle
 | |
| 	}
 | |
| 	if e.Date == (time.Time{}) {
 | |
| 		return []byte{}, ErrorMissingDate
 | |
| 	}
 | |
| 
 | |
| 	out := map[string]any{}
 | |
| 	out["title"] = e.Title
 | |
| 	out["date"] = e.Date.Format(time.RFC3339)
 | |
| 	for _, f := range e.Fields {
 | |
| 		if _, ok := out[f.Key]; !ok {
 | |
| 			if f.Key == "json" {
 | |
| 				ob := map[string]any{}
 | |
| 				if j, ok := f.Value.(json.RawMessage); ok {
 | |
| 					json.Unmarshal(j, &ob)
 | |
| 				}
 | |
| 				// If we couldn't get valid data from there, this will just be empty
 | |
| 				for k, v := range ob {
 | |
| 					if k != "title" && k != "date" {
 | |
| 						out[k] = v
 | |
| 					}
 | |
| 				}
 | |
| 			} else {
 | |
| 				out[f.Key] = f.Value
 | |
| 				if vt, ok := f.Value.(time.Time); ok {
 | |
| 					out[f.Key] = vt.Format(time.RFC3339)
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return json.Marshal(out)
 | |
| }
 | |
| 
 | |
| func (e *Entry) unmarshalJsonChanHelper(m map[string]any, ch chan Meta, wg *sync.WaitGroup) {
 | |
| 	for k, v := range m {
 | |
| 		wg.Add(1)
 | |
| 		go func(key string, value any) {
 | |
| 			defer wg.Done()
 | |
| 			if key != "json" {
 | |
| 				ch <- Meta{key, value}
 | |
| 				return
 | |
| 			}
 | |
| 			subM := map[string]any{}
 | |
| 			if s, ok := value.(string); ok {
 | |
| 				dec := json.NewDecoder(strings.NewReader(s))
 | |
| 				dec.UseNumber()
 | |
| 				dec.Decode(&subM)
 | |
| 			} else {
 | |
| 				subM = value.(map[string]any)
 | |
| 			}
 | |
| 			e.unmarshalJsonChanHelper(subM, ch, wg)
 | |
| 		}(k, v)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (e *Entry) getUnmarshalJsonChan(m map[string]any) chan Meta {
 | |
| 	ch := make(chan Meta, len(m))
 | |
| 	var wg sync.WaitGroup
 | |
| 
 | |
| 	e.unmarshalJsonChanHelper(m, ch, &wg)
 | |
| 	go func() {
 | |
| 		wg.Wait()
 | |
| 		close(ch)
 | |
| 	}()
 | |
| 
 | |
| 	return ch
 | |
| }
 | |
| 
 | |
| func (e *Entry) UnmarshalJSON(in []byte) error {
 | |
| 	out := map[string]any{}
 | |
| 	dec := json.NewDecoder(bytes.NewReader(in))
 | |
| 	dec.UseNumber()
 | |
| 	err := dec.Decode(&out)
 | |
| 	if err != nil {
 | |
| 		return newParsingError(err)
 | |
| 	}
 | |
| 	title, ok := out["title"].(string)
 | |
| 	if (!ok || title == "") && !e.skipMissing {
 | |
| 		return ErrorMissingTitle
 | |
| 	}
 | |
| 	e.Title = title
 | |
| 	dates, ok := out["date"].(string)
 | |
| 	if (!ok || dates == "") && !e.skipMissing {
 | |
| 		return ErrorMissingDate
 | |
| 	}
 | |
| 	date, err := tools.ParseDate(dates)
 | |
| 	if err != nil && !e.skipMissing {
 | |
| 		return newParsingError(err)
 | |
| 	}
 | |
| 	e.Date = date
 | |
| 	ch := e.getUnmarshalJsonChan(out)
 | |
| 	for m := range ch {
 | |
| 		if m.Key == "title" || m.Key == "date" {
 | |
| 			continue
 | |
| 		} else if vs, ok := m.Value.(string); ok {
 | |
| 			if vd, err := tools.ParseDate(vs); err == nil {
 | |
| 				m.Value = vd
 | |
| 			} else {
 | |
| 				m.Value = vs
 | |
| 			}
 | |
| 		} else if n, ok := m.Value.(json.Number); ok {
 | |
| 			it, _ := n.Int64()
 | |
| 			fl, _ := n.Float64()
 | |
| 			if float64(it) == fl {
 | |
| 				m.Value = it
 | |
| 			} else {
 | |
| 				m.Value = fl
 | |
| 			}
 | |
| 		}
 | |
| 		e.Fields = append(e.Fields, m)
 | |
| 	}
 | |
| 	return nil
 | |
| }
 |