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("\n@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 := time.Parse(time.RFC3339, date) if e != nil { d, e = time.Parse(DateFormat, 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 := time.Parse(time.RFC3339, 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 := time.Parse(time.RFC3339, 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 }