Compare commits
47 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fba5551bb3 | |||
| 85671a076c | |||
| da35bf4bcf | |||
| 345ef9471d | |||
| 5f6ce7ca21 | |||
| 514ffee613 | |||
| ece9256745 | |||
| e9c1bf7070 | |||
| 306b2c597d | |||
| ab0dea2805 | |||
| 7312714ccb | |||
| 9980ae8cc7 | |||
| 02a7babb36 | |||
| 80e07dbd14 | |||
| 9ca2b9156d | |||
| b456bcbfcc | |||
| bddf8e1458 | |||
| 396f19f691 | |||
| 2e70a10d94 | |||
| 821ba6117c | |||
| 632c7143f1 | |||
| 79fa957d02 | |||
| 8086029b03 | |||
| d6482952a4 | |||
| e67f159ce7 | |||
| 1962e1db50 | |||
| 4c0edcd1a5 | |||
| d1b3604e1e | |||
| 8d238d0d76 | |||
| 59634f6c3f | |||
| 820a2de269 | |||
| 7e54d6e46c | |||
| b89baa3f82 | |||
| 40b2dd5793 | |||
| 33fbdf7ecb | |||
| 17da5b66ea | |||
| febbce8a6b | |||
| 11dea95ce2 | |||
| 5b8e4696ea | |||
| 2fc60c16c6 | |||
| f68aebdedb | |||
| da3b524925 | |||
| 99f6dc3f8c | |||
| 89e6c2b3bd | |||
| 286ac4557d | |||
| fd5d315164 | |||
| 9e1bc2ccb0 |
33 changed files with 1228 additions and 130 deletions
22
CHANGELOG.md
22
CHANGELOG.md
|
|
@ -1,5 +1,27 @@
|
|||
# Changelog
|
||||
|
||||
## [0.0.6] - 2024-10-07
|
||||
|
||||
- Update external dependency: go-dateparser
|
||||
|
||||
## [0.0.5] - 2024-10-07
|
||||
|
||||
- Small change: adds --output_json to drop command.
|
||||
|
||||
## [0.0.4] - 2024-05-08
|
||||
|
||||
- ✨ Add -p flag to config to print config path
|
||||
|
||||
## [0.0.3] - 2024-03-11
|
||||
|
||||
- ✨ Add JSON formatter
|
||||
- 💥 Breaking change: renamed output.stdout.config value formatter to format
|
||||
|
||||
## [0.0.2] - 2024-03-09
|
||||
|
||||
- ✨ Use plain formatter to output entry from drop
|
||||
- ✨ Add newline to file when needed
|
||||
|
||||
## [0.0.1] - 2024-03-02
|
||||
|
||||
🎉 Initial release.
|
||||
|
|
|
|||
13
Makefile
13
Makefile
|
|
@ -3,6 +3,12 @@ OUT=my-log
|
|||
GOBIN=$(shell go env GOBIN)
|
||||
COVEROUT=cover.out
|
||||
COVERHTML=cover.html
|
||||
OPEN=xdg-open
|
||||
OS=$(shell uname -s)
|
||||
|
||||
ifeq ($(OS),Darwin)
|
||||
OPEN=open
|
||||
endif
|
||||
|
||||
.PHONY: help
|
||||
help: ## Show help for documented recipes
|
||||
|
|
@ -23,7 +29,8 @@ test: ## Test application and generate coverage report
|
|||
$(MAKE) clean
|
||||
$(MAKE) $(COVEROUT)
|
||||
|
||||
$(COVEROUT): $(SOURCES) fmt
|
||||
$(COVEROUT): $(SOURCES)
|
||||
$(MAKE) fmt
|
||||
go test ./... -race -cover -coverprofile $@
|
||||
|
||||
$(COVERHTML): $(COVEROUT)
|
||||
|
|
@ -33,8 +40,8 @@ $(COVERHTML): $(COVEROUT)
|
|||
report: $(COVERHTML) ## Generate a coverage report
|
||||
|
||||
.PHONY: open-report
|
||||
open-report: report ## Open the coverage report in the default browser
|
||||
xdg-open $(COVERHTML)
|
||||
open-report: $(COVERHTML) ## Open the coverage report in the default browser
|
||||
$(OPEN) $<
|
||||
|
||||
.PHONY: build
|
||||
build: $(OUT) ## Builds the application
|
||||
|
|
|
|||
156
README.md
Normal file
156
README.md
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
# my-log
|
||||
|
||||
`my-log` is a tool for generating and parsing log files for whatever you want. This is early in development. Check our Roadmap before for what's working.
|
||||
|
||||
I originally wrote [DropLogger](https://github.com/goodevilgenius/droplogger) to serve this purpose. DropLogger was originally designed to work primarily with IFTTT and Dropbox. Due to IFTTT changing their service significantly since I originally signed up, I no longer use it. Even without IFTTT, DropLogger is a great tool.
|
||||
|
||||
So, why did I decide to completely rewrite it? Mainly because DropLogger is written in Python. While Python is a great language, I haven't used it seriously for many years, and I didn't find a whole lot of motivation to add new features to DropLogger, due to this. But I've been working in go for the past six months, and have kind of fell in love with the language. I'd been considering a rewrite of DropLogger for a while, so I decided to help myself get more practice in go by rewriting DropLogger in it.
|
||||
|
||||
So, how does this work?
|
||||
|
||||
Currently, it mostly doesn't. `my-log` is still in its early stages, and DropLogger is still needed for most of the functionality. So, how will it work?
|
||||
|
||||
## Log files
|
||||
|
||||
We start with the individual log files. These were designed to be very flexible so that they could be written using a number of different tools. Originally, IFTTT recipes were created that would write to files in Dropbox, but this could be adapted to a number of other automation tools to automatically write as things happen.
|
||||
|
||||
What things? Well, maybe you use Tasker to trigger an action when you get home. You might want to keep a log of whenever you arrive at your house. Or, maybe you use [Last.FM](https://www.last.fm/home) to keep track of your music listening habits, and you want to log whenever you listen to a music track. You could create a Zap in Zapier that responds to new scrobbles on Last.FM, and adds those scrobbles to a file.
|
||||
|
||||
### Log format
|
||||
|
||||
As I mentioned, the format is intended to be very easy to write. Here's a sample:
|
||||
|
||||
```
|
||||
@begin January 12, 2024 at 2:34PM - Title
|
||||
@key value
|
||||
@longKey this entry is long, and
|
||||
spans multiple lines
|
||||
@number 4
|
||||
@bool true
|
||||
@end
|
||||
```
|
||||
|
||||
So, each entry starts with `@begin` and ends with `@end`. It must have a date and a title. It may also have additional data which is indicated by an `@` at the beginning of the line. If I were to convert this to JSON (which `my-log` can do for you), it would look like:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Title",
|
||||
"date": "2024-01-12T14:34:00Z",
|
||||
"key": "value",
|
||||
"longKey": "this entry is long, and\nspans multiple lines",
|
||||
"number": 4,
|
||||
"bool": true
|
||||
}
|
||||
```
|
||||
|
||||
A couple things to note:
|
||||
- When outputting JSON, the date is converted to ISO-8601 format. The timezone used (if none was given in the original log) is your own local time.
|
||||
- The newline in the `longKey` was preserved
|
||||
- Different types are recognized and parsed correctly. It supports the following types:
|
||||
+ string (default)
|
||||
+ numbers
|
||||
+ boolean values (true or false)
|
||||
+ dates and times
|
||||
+ A null value (the string "null", "nil", "none", or "~")
|
||||
+ A raw JSON object/array
|
||||
|
||||
Since the extra fields are optional, the simplest log entry can be on a single line. For example, you might have a log file called `notes.txt` with this:
|
||||
|
||||
```
|
||||
@begin February 3, 2015 at 01:33PM - Remember to call Mom @end
|
||||
@begin February 4, 2015 at 07:45AM - Breakfast today was great! @end
|
||||
```
|
||||
|
||||
As JSON, that would be:
|
||||
|
||||
```json
|
||||
[{
|
||||
"title":"Rember to call Mom",
|
||||
"date":"2015-02-03T13:33:00Z"
|
||||
},{
|
||||
"title":"Breakfase today was great!",
|
||||
"date":"2015-02-04T07:45:00Z"
|
||||
}]
|
||||
```
|
||||
|
||||
### Adding log entries
|
||||
|
||||
As was previously noted, the idea is that you can figure out the best way for you to add to the log file. But, `my-log` also comes with a command to add them from the command line. Run `my-log drop --help` for instructions on how to use it. But, here's a few examples:
|
||||
|
||||
```shell
|
||||
my-log drop notes "Hello"
|
||||
# Adds "@begin <date> - Hello @end" to notes.txt file
|
||||
|
||||
my-log drop -d "yesterday" calls "Talked with Jeremy" -f phone_number=+1-555-867-5309
|
||||
# If today is January 2, 2024, adds the follow entry to calls.txt
|
||||
# @begin January 1, 2024 at 12:00:00AM UTC - Talked with Jeremy
|
||||
# @phone_number +1-555-867-5309 @end
|
||||
|
||||
my-log drop -d "1999-12-31T23:59:59Z" events "The end of the world" -f notes="As we know it" -j '{"artist":"R.E.M","slaps":true}'
|
||||
# Adds the following entry to events.txt
|
||||
# @begin December 31, 1999 at 11:59:59PM UTC - The end of the world
|
||||
# @notes As we know it
|
||||
# @artist R.E.M
|
||||
# @slaps true @end
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
This is a work in progress. More info coming soon. Short version is, we want to be able to output to multiple formats in multiple places.
|
||||
|
||||
Check [DropLogger's documentation](https://github.com/goodevilgenius/droplogger?tab=readme-ov-file#output) for info on how we want to make it work.
|
||||
|
||||
## Configuration
|
||||
|
||||
We use a TOML file for configuration. The default location on Linux is ~/.config/my-log/config.toml. You can find the exact location by doing `my-log -h` and looking at the help for the `--config` flag. Running `my-log config` will save the default config file to the default location. The file is intended to be edited by hand. There is no mechanism within the program to modify the file, aside from saving the default one.
|
||||
|
||||
The default one has comments to help you out, but here's the options:
|
||||
|
||||
### `[input]`
|
||||
|
||||
- `path`: The path to where the logs are located. This is usually ~/my-log, but if you want to store it in Dropbox, you might want it to be ~/Dropbox/my-log
|
||||
- `ext`: The file extension for log files. This is usually txt, which makes it easier to work with multiple tools, but you can change it to log, or my-log, if you want. If you set it to an empty string, no extension will be used, which also means that when parsing the log files, it will look at all files in the folder.
|
||||
- `recurse`: Whether to look in sub-folders.
|
||||
|
||||
### `[output.which-one]`
|
||||
|
||||
Each separate output has its own set of configuration. So, replace `which-one` with the output name.
|
||||
|
||||
- `enabled`: if set to false, will skip that output when running.
|
||||
- `config`: This is an output-specific set of settings
|
||||
|
||||
#### `[output.stdout.config]`
|
||||
|
||||
*This section may change in the near future. We're considering supporting multiple formats.*
|
||||
|
||||
- `format`: Which formatter to use when outputting data. This value is also used by `my-log drop` to output the new entry.
|
||||
|
||||
### `[formatters]`
|
||||
|
||||
Some formatters may have custom configuration.
|
||||
|
||||
#### `[formatters.json]`
|
||||
|
||||
- `pretty_print`: If true, JSON output will be pretty printed. If false, it will be printed to a single line.
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [x] `drop` command. This is functional, and supports all the features of `drop-a-log`
|
||||
+ [x] Don't add an extra blank line before new entries
|
||||
+ [x] Add a new line at the end
|
||||
- [ ] Output log entries
|
||||
+ [ ] A single date
|
||||
+ [ ] a specific period of time
|
||||
+ [ ] filter to specific logs
|
||||
+ [ ] stdout
|
||||
- [x] plain text
|
||||
- [x] JSON
|
||||
- [ ] YAML
|
||||
- [ ] Other formats? Submit an issue!
|
||||
+ [ ] file output
|
||||
- [ ] Any format that stdout supports
|
||||
- [ ] Multiple formats at once
|
||||
- [ ] RSS
|
||||
- [ ] ATOM
|
||||
+ [ ] sqlite database
|
||||
- [ ] Maybe: plug-in system to add formats or output destinations
|
||||
|
|
@ -32,6 +32,11 @@ var configCmd = &cobra.Command{
|
|||
//Long: ``,
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
print, _ := cmd.Flags().GetBool("print")
|
||||
if print {
|
||||
fmt.Fprintln(cmd.OutOrStdout(), config.ConfigPath)
|
||||
return nil
|
||||
}
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
if !force {
|
||||
_, err = os.Stat(config.ConfigPath)
|
||||
|
|
@ -60,4 +65,5 @@ func init() {
|
|||
rootCmd.AddCommand(configCmd)
|
||||
|
||||
configCmd.Flags().BoolP("force", "f", false, "Force overwrite")
|
||||
configCmd.Flags().BoolP("print", "p", false, "Print path only")
|
||||
}
|
||||
|
|
|
|||
30
cmd/drop.go
30
cmd/drop.go
|
|
@ -21,7 +21,9 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"codeberg.org/danjones000/my-log/config"
|
||||
"codeberg.org/danjones000/my-log/files"
|
||||
"codeberg.org/danjones000/my-log/formatters"
|
||||
"codeberg.org/danjones000/my-log/models"
|
||||
"codeberg.org/danjones000/my-log/tools"
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -30,6 +32,7 @@ import (
|
|||
var d Date
|
||||
var fields map[string]string
|
||||
var j Json
|
||||
var outJson bool
|
||||
|
||||
// dropCmd represents the drop command
|
||||
var dropCmd = &cobra.Command{
|
||||
|
|
@ -39,30 +42,42 @@ var dropCmd = &cobra.Command{
|
|||
Args: cobra.ExactArgs(2),
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if outJson {
|
||||
config.Overrides["output.stdout.config.format"] = "json"
|
||||
}
|
||||
|
||||
log := args[0]
|
||||
title := args[1]
|
||||
e := models.PartialEntry()
|
||||
ms := &models.Metas{}
|
||||
if len(j.RawMessage) > 8 {
|
||||
err := json.Unmarshal([]byte(j.RawMessage), &e)
|
||||
err := json.Unmarshal([]byte(j.RawMessage), ms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for k, v := range fields {
|
||||
e.Fields = append(e.Fields, models.Meta{k, tools.ParseString(v)})
|
||||
ms.AppendTo(k, tools.ParseString(v))
|
||||
}
|
||||
e.Title = title
|
||||
e.Date = d.t
|
||||
e := models.Entry{title, d.t, *ms}
|
||||
l := models.Log{log, []models.Entry{e}}
|
||||
err := files.Append(l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
by, err := e.MarshalText()
|
||||
|
||||
form, err := formatters.Preferred()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "%s\n", by)
|
||||
out, err := form.Log(l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(out) > 0 && out[len(out)-1] != 10 {
|
||||
out = append(out, 10)
|
||||
}
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "%s", out)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
|
@ -74,6 +89,7 @@ func init() {
|
|||
dropCmd.Flags().VarP(&d, "date", "d", "Date for log entry")
|
||||
dropCmd.Flags().StringToStringVarP(&fields, "fields", "f", nil, "Fields you add to entry")
|
||||
dropCmd.Flags().VarP(&j, "json", "j", "Entire entry as json")
|
||||
dropCmd.Flags().BoolVarP(&outJson, "output_json", "o", false, "Output result as JSON")
|
||||
}
|
||||
|
||||
type Json struct {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ func init() {
|
|||
// will be global for your application.
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(&config.ConfigPath, "config", "c", config.ConfigPath, "config file")
|
||||
rootCmd.PersistentFlags().StringToStringVarP(&config.Overrides, "config-value", "v", config.Overrides, "Override config values. Use dot syntax to specify key. E.g. -v output.stdout.config.json=true")
|
||||
rootCmd.PersistentFlags().StringToStringVarP(&config.Overrides, "config-value", "v", config.Overrides, "Override config values. Use dot syntax to specify key. E.g. -v output.stdout.config.format=json")
|
||||
|
||||
// Cobra also supports local flags, which will only run
|
||||
// when this action is called directly.
|
||||
|
|
|
|||
|
|
@ -27,8 +27,14 @@ dotFolder = true
|
|||
[output.stdout]
|
||||
enabled = true
|
||||
[output.stdout.config]
|
||||
# Whether to output as JSON. Maybe useful to pipe elsewhere.
|
||||
json = false
|
||||
# Formatter to use when outputting to stdout
|
||||
format = "plain"
|
||||
|
||||
[formatters]
|
||||
|
||||
[formatters.json]
|
||||
# Set to true to pretty print JSON output
|
||||
pretty_print = false
|
||||
|
||||
`
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ func Load() (Config, error) {
|
|||
}
|
||||
env.Parse(&c)
|
||||
c.Outputs["stdout"] = loadStdout(c.Outputs["stdout"])
|
||||
c.Formatters["json"] = loadJsonFormat(c.Formatters["json"])
|
||||
|
||||
l := ""
|
||||
for k, v := range Overrides {
|
||||
|
|
@ -77,3 +78,21 @@ func (oo Outputs) Stdout() (s Stdout, enabled bool) {
|
|||
|
||||
return
|
||||
}
|
||||
|
||||
func loadJsonFormat(c map[string]any) map[string]any {
|
||||
jf := JsonFormat{}
|
||||
mapst.Decode(c, &jf)
|
||||
env.Parse(&jf)
|
||||
mapst.Decode(jf, &c)
|
||||
return c
|
||||
}
|
||||
|
||||
func (ff Formatters) Json() (jf JsonFormat) {
|
||||
o, ok := ff["json"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
mapst.Decode(o, &jf)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,12 @@ func TestOverrideJson(t *testing.T) {
|
|||
assert.Equal(t, "txt", c.Input.Ext)
|
||||
}
|
||||
|
||||
// @todo test time
|
||||
func TestTimeParse(t *testing.T) {
|
||||
Overrides = map[string]string{"input.ext": "now"}
|
||||
c, err := Load()
|
||||
assert.ErrorContains(t, err, "incompatible types: TOML value has type time.Time; destination has type string")
|
||||
assert.Equal(t, "txt", c.Input.Ext)
|
||||
}
|
||||
|
||||
func TestStdoutMissing(t *testing.T) {
|
||||
var oo Outputs = map[string]Output{}
|
||||
|
|
@ -65,12 +70,25 @@ func TestStdoutMissing(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestStdoutLoad(t *testing.T) {
|
||||
os.Setenv("LOG_STDOUT_JSON", "true")
|
||||
defer os.Unsetenv("LOG_STDOUT_JSON")
|
||||
os.Setenv("LOG_STDOUT_FORMAT", "json")
|
||||
defer os.Unsetenv("LOG_STDOUT_FORMAT")
|
||||
os.Setenv("LOG_STDOUT_ENABLED", "true")
|
||||
defer os.Unsetenv("LOG_STDOUT_ENABLED")
|
||||
c, _ := Load()
|
||||
std, en := c.Outputs.Stdout()
|
||||
assert.True(t, en)
|
||||
assert.True(t, std.Json)
|
||||
assert.Equal(t, "json", std.Format)
|
||||
}
|
||||
|
||||
func TestFormatJson(t *testing.T) {
|
||||
ff := Formatters{
|
||||
"json": map[string]any{"pretty_print": true},
|
||||
}
|
||||
|
||||
js := ff.Json()
|
||||
assert.True(t, js.PrettyPrint)
|
||||
|
||||
ff = Formatters{}
|
||||
js = ff.Json()
|
||||
assert.False(t, js.PrettyPrint)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
package config
|
||||
|
||||
type Config struct {
|
||||
Input Input
|
||||
Outputs Outputs `toml:"output"`
|
||||
Input Input
|
||||
Outputs Outputs `toml:"output"`
|
||||
Formatters Formatters
|
||||
}
|
||||
|
||||
type Input struct {
|
||||
|
|
@ -20,9 +21,15 @@ type Output struct {
|
|||
}
|
||||
|
||||
type Stdout struct {
|
||||
Json bool `env:"LOG_STDOUT_JSON" mapstructure:"json"`
|
||||
Format string `env:"LOG_STDOUT_FORMAT" mapstructure:"format"`
|
||||
}
|
||||
|
||||
type stdoutEnabled struct {
|
||||
Enabled bool `env:"LOG_STDOUT_ENABLED"`
|
||||
}
|
||||
|
||||
type Formatters map[string]map[string]any
|
||||
|
||||
type JsonFormat struct {
|
||||
PrettyPrint bool `env:"LOG_JSON_PRETTY_PRINT" mapstructure:"pretty_print"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package files
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
fp "path/filepath"
|
||||
"strings"
|
||||
|
|
@ -31,18 +32,32 @@ func Append(l models.Log) error {
|
|||
return err
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0640)
|
||||
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0640)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
f.Seek(-1, os.SEEK_END)
|
||||
last := make([]byte, 1, 1)
|
||||
n, err := f.Read(last)
|
||||
if err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
|
||||
if err == nil && n > 0 {
|
||||
if last[0] != 10 {
|
||||
f.Write([]byte{10})
|
||||
}
|
||||
}
|
||||
|
||||
for _, e := range l.Entries {
|
||||
by, err := e.MarshalText()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
f.Write(by)
|
||||
f.Write([]byte{10})
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ func (s *AppendTestSuite) TearDownSuite() {
|
|||
}
|
||||
|
||||
func (s *AppendTestSuite) TestSuccess() {
|
||||
defer os.Remove(s.dir + "/test.log")
|
||||
when := time.Now().Local()
|
||||
e := models.Entry{
|
||||
Title: "Jimmy",
|
||||
|
|
@ -57,6 +58,82 @@ func (s *AppendTestSuite) TestSuccess() {
|
|||
s.Assert().Contains(st, "\n@bar true")
|
||||
}
|
||||
|
||||
func (s *AppendTestSuite) TestTwoEntries() {
|
||||
defer os.Remove(s.dir + "/test.log")
|
||||
when := time.Now().Local()
|
||||
whens := when.Format(models.DateFormat)
|
||||
e := []models.Entry{
|
||||
{Title: "one", Date: when},
|
||||
{Title: "two", Date: when},
|
||||
}
|
||||
l := models.Log{
|
||||
Name: "test",
|
||||
Entries: e,
|
||||
}
|
||||
err := Append(l)
|
||||
s.Assert().NoError(err)
|
||||
s.Require().FileExists(s.dir + "/test.log")
|
||||
by, _ := os.ReadFile(s.dir + "/test.log")
|
||||
exp := fmt.Sprintf("@begin %s - one @end\n@begin %s - two @end\n", whens, whens)
|
||||
s.Assert().Equal(exp, string(by))
|
||||
}
|
||||
|
||||
func (s *AppendTestSuite) TestAddNewLine() {
|
||||
defer os.Remove(s.dir + "/test.log")
|
||||
os.WriteFile(s.dir+"/test.log", []byte("foo"), 0644)
|
||||
when := time.Now().Local()
|
||||
whens := when.Format(models.DateFormat)
|
||||
e := []models.Entry{
|
||||
{Title: "one", Date: when},
|
||||
}
|
||||
l := models.Log{
|
||||
Name: "test",
|
||||
Entries: e,
|
||||
}
|
||||
err := Append(l)
|
||||
s.Assert().NoError(err)
|
||||
s.Require().FileExists(s.dir + "/test.log")
|
||||
by, _ := os.ReadFile(s.dir + "/test.log")
|
||||
exp := fmt.Sprintf("foo\n@begin %s - one @end\n", whens)
|
||||
s.Assert().Equal(exp, string(by))
|
||||
}
|
||||
|
||||
func (s *AppendTestSuite) TestDontAddNewLine() {
|
||||
defer os.Remove(s.dir + "/test.log")
|
||||
os.WriteFile(s.dir+"/test.log", []byte("foo\n"), 0644)
|
||||
when := time.Now().Local()
|
||||
whens := when.Format(models.DateFormat)
|
||||
e := []models.Entry{
|
||||
{Title: "one", Date: when},
|
||||
}
|
||||
l := models.Log{
|
||||
Name: "test",
|
||||
Entries: e,
|
||||
}
|
||||
err := Append(l)
|
||||
s.Assert().NoError(err)
|
||||
s.Require().FileExists(s.dir + "/test.log")
|
||||
by, _ := os.ReadFile(s.dir + "/test.log")
|
||||
exp := fmt.Sprintf("foo\n@begin %s - one @end\n", whens)
|
||||
s.Assert().Equal(exp, string(by))
|
||||
}
|
||||
|
||||
func (s *AppendTestSuite) TestFailEntry() {
|
||||
defer os.Remove(s.dir + "/test.log")
|
||||
e := models.Entry{
|
||||
Title: "Jimmy",
|
||||
}
|
||||
l := models.Log{
|
||||
Name: "test",
|
||||
Entries: []models.Entry{e},
|
||||
}
|
||||
err := Append(l)
|
||||
s.Assert().NoError(err)
|
||||
s.Require().FileExists(s.dir + "/test.log")
|
||||
by, _ := os.ReadFile(s.dir + "/test.log")
|
||||
s.Assert().Equal([]byte{}, by)
|
||||
}
|
||||
|
||||
func (s *AppendTestSuite) TestDotFolder() {
|
||||
config.Overrides["input.dotFolder"] = "true"
|
||||
e := models.Entry{
|
||||
|
|
@ -134,7 +211,7 @@ func (s *AppendTestSuite) TestConfLoadErr() {
|
|||
|
||||
func (s *AppendTestSuite) TestMkdirErr() {
|
||||
// Don't run this test as root
|
||||
config.Overrides["input.path"] = "/root/my-logs-test"
|
||||
config.Overrides["input.path"] = "/var/my-logs-test"
|
||||
defer func(path string) {
|
||||
config.Overrides["input.path"] = path
|
||||
}(s.dir)
|
||||
|
|
|
|||
11
formatters/interface.go
Normal file
11
formatters/interface.go
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
package formatters
|
||||
|
||||
import "codeberg.org/danjones000/my-log/models"
|
||||
|
||||
type Formatter interface {
|
||||
Name() string
|
||||
Logs([]models.Log) (out []byte, err error)
|
||||
Log(models.Log) (out []byte, err error)
|
||||
Entry(models.Entry) (out []byte, err error)
|
||||
Meta(models.Meta) (out []byte, err error)
|
||||
}
|
||||
77
formatters/json.go
Normal file
77
formatters/json.go
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
package formatters
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"codeberg.org/danjones000/my-log/config"
|
||||
"codeberg.org/danjones000/my-log/models"
|
||||
)
|
||||
|
||||
func newJson(ff config.Formatters) (Formatter, error) {
|
||||
return &Json{ff.Json().PrettyPrint}, nil
|
||||
}
|
||||
|
||||
type Json struct {
|
||||
prettPrint bool
|
||||
}
|
||||
|
||||
func (js *Json) Name() string {
|
||||
return "json"
|
||||
}
|
||||
|
||||
func (js *Json) marshal(v any) (o []byte, err error) {
|
||||
o, err = json.Marshal(v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if js.prettPrint {
|
||||
buff := &bytes.Buffer{}
|
||||
err = json.Indent(buff, o, "", "\t")
|
||||
if err == nil {
|
||||
o = buff.Bytes()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (js *Json) Meta(m models.Meta) ([]byte, error) {
|
||||
o := map[string]any{m.Key: m.Value}
|
||||
return js.marshal(o)
|
||||
}
|
||||
|
||||
func (js *Json) entryMap(e models.Entry) map[string]any {
|
||||
o := map[string]any{
|
||||
"title": e.Title,
|
||||
"date": e.Date.Format(time.RFC3339),
|
||||
}
|
||||
for _, m := range e.Fields {
|
||||
o[m.Key] = m.Value
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func (js *Json) Entry(e models.Entry) ([]byte, error) {
|
||||
return js.marshal(js.entryMap(e))
|
||||
}
|
||||
|
||||
func (js *Json) Log(l models.Log) ([]byte, error) {
|
||||
return js.Logs([]models.Log{l})
|
||||
}
|
||||
|
||||
func (js *Json) Logs(logs []models.Log) (out []byte, err error) {
|
||||
if len(logs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
o := map[string][]map[string]any{}
|
||||
for _, l := range logs {
|
||||
es := []map[string]any{}
|
||||
for _, e := range l.Entries {
|
||||
es = append(es, js.entryMap(e))
|
||||
}
|
||||
o[l.Name] = es
|
||||
}
|
||||
return js.marshal(o)
|
||||
}
|
||||
81
formatters/json_test.go
Normal file
81
formatters/json_test.go
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
package formatters
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"codeberg.org/danjones000/my-log/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestJsonName(t *testing.T) {
|
||||
f, _ := New("json")
|
||||
assert.Equal(t, "json", f.Name())
|
||||
}
|
||||
|
||||
func TestJsonMeta(t *testing.T) {
|
||||
f, _ := New("json")
|
||||
m := models.Meta{"foo", 42}
|
||||
exp := `{"foo":42}`
|
||||
o, err := f.Meta(m)
|
||||
assert.NoError(t, err)
|
||||
assert.JSONEq(t, exp, string(o))
|
||||
}
|
||||
|
||||
func TestJsonEntry(t *testing.T) {
|
||||
when := time.Now()
|
||||
f, _ := New("json")
|
||||
m := models.Meta{"foo", 42}
|
||||
e := models.Entry{
|
||||
Title: "Homer",
|
||||
Date: when,
|
||||
Fields: []models.Meta{m},
|
||||
}
|
||||
exp := fmt.Sprintf(`{"title":"%s","date":"%s","foo":42}`, e.Title, when.Format(time.RFC3339))
|
||||
o, err := f.Entry(e)
|
||||
assert.NoError(t, err)
|
||||
assert.JSONEq(t, exp, string(o))
|
||||
}
|
||||
|
||||
func TestJsonLog(t *testing.T) {
|
||||
when := time.Now()
|
||||
f, _ := New("json")
|
||||
m := models.Meta{"foo", 42}
|
||||
e := models.Entry{
|
||||
Title: "Homer",
|
||||
Date: when,
|
||||
Fields: []models.Meta{m},
|
||||
}
|
||||
l := models.Log{"stuff", []models.Entry{e}}
|
||||
exp := fmt.Sprintf(`{"%s":[{"title":"%s","date":"%s","foo":42}]}`, l.Name, e.Title, when.Format(time.RFC3339))
|
||||
o, err := f.Log(l)
|
||||
assert.NoError(t, err)
|
||||
assert.JSONEq(t, exp, string(o))
|
||||
}
|
||||
|
||||
func TestJsonNoLogs(t *testing.T) {
|
||||
f, _ := New("json")
|
||||
o, err := f.Logs([]models.Log{})
|
||||
var exp []byte
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, exp, o)
|
||||
}
|
||||
|
||||
func TestJsonErr(t *testing.T) {
|
||||
f, _ := New("json")
|
||||
o, err := f.Meta(models.Meta{"foo", make(chan bool)})
|
||||
var exp []byte
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, exp, o)
|
||||
}
|
||||
|
||||
func TestJsonPretty(t *testing.T) {
|
||||
f := Json{true}
|
||||
o, err := f.Meta(models.Meta{"foo", 42})
|
||||
exp := `{
|
||||
"foo": 42
|
||||
}`
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, exp, string(o))
|
||||
}
|
||||
45
formatters/new.go
Normal file
45
formatters/new.go
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package formatters
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"codeberg.org/danjones000/my-log/config"
|
||||
)
|
||||
|
||||
type formatMaker func(config.Formatters) (Formatter, error)
|
||||
|
||||
var formatterMap = map[string]formatMaker{
|
||||
"plain": newPlain,
|
||||
"json": newJson,
|
||||
"zero": newNull,
|
||||
}
|
||||
|
||||
func Preferred() (f Formatter, err error) {
|
||||
conf, err := config.Load()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
std, _ := conf.Outputs.Stdout()
|
||||
return New(std.Format)
|
||||
}
|
||||
|
||||
func New(kind string) (f Formatter, err error) {
|
||||
conf, err := config.Load()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if make, ok := formatterMap[kind]; ok {
|
||||
return make(conf.Formatters)
|
||||
}
|
||||
|
||||
return nil, errors.New("unimplemented")
|
||||
}
|
||||
|
||||
func Kinds() []string {
|
||||
r := []string{}
|
||||
for kind, _ := range formatterMap {
|
||||
r = append(r, kind)
|
||||
}
|
||||
return r
|
||||
}
|
||||
45
formatters/new_test.go
Normal file
45
formatters/new_test.go
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package formatters
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"codeberg.org/danjones000/my-log/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestKinds(t *testing.T) {
|
||||
assert.ElementsMatch(t, []string{"plain", "json", "zero"}, Kinds())
|
||||
}
|
||||
|
||||
func TestNewUnsupported(t *testing.T) {
|
||||
f, err := New("nope")
|
||||
assert.Nil(t, f)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNewCantGetConfig(t *testing.T) {
|
||||
f, _ := os.CreateTemp("", "test")
|
||||
oldConf := config.ConfigPath
|
||||
config.ConfigPath = f.Name()
|
||||
defer f.Close()
|
||||
defer func() {
|
||||
config.ConfigPath = oldConf
|
||||
}()
|
||||
|
||||
fmt.Fprint(f, `{"not":"toml"}`)
|
||||
form, err := New("plain")
|
||||
assert.Nil(t, form)
|
||||
assert.Error(t, err)
|
||||
|
||||
form, err = Preferred()
|
||||
assert.Nil(t, form)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestPreferred(t *testing.T) {
|
||||
form, err := Preferred()
|
||||
assert.NotNil(t, form)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
32
formatters/null.go
Normal file
32
formatters/null.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package formatters
|
||||
|
||||
import (
|
||||
"codeberg.org/danjones000/my-log/config"
|
||||
"codeberg.org/danjones000/my-log/models"
|
||||
)
|
||||
|
||||
func newNull(ff config.Formatters) (Formatter, error) {
|
||||
return &Null{}, nil
|
||||
}
|
||||
|
||||
type Null struct{}
|
||||
|
||||
func (n *Null) Name() string {
|
||||
return "zero"
|
||||
}
|
||||
|
||||
func (n *Null) Meta(m models.Meta) (o []byte, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (n *Null) Entry(e models.Entry) (o []byte, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (n *Null) Log(l models.Log) (o []byte, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (n *Null) Logs(logs []models.Log) (out []byte, err error) {
|
||||
return
|
||||
}
|
||||
45
formatters/null_test.go
Normal file
45
formatters/null_test.go
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package formatters
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"codeberg.org/danjones000/my-log/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var empty []byte
|
||||
|
||||
func TestNullName(t *testing.T) {
|
||||
f, err := New("zero")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "zero", f.Name())
|
||||
}
|
||||
|
||||
func TestNullMeta(t *testing.T) {
|
||||
f, _ := New("zero")
|
||||
o, err := f.Meta(models.Meta{"foo", 42})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, empty, o)
|
||||
}
|
||||
|
||||
func TestNullEntry(t *testing.T) {
|
||||
f, _ := New("zero")
|
||||
o, err := f.Entry(models.Entry{"title", time.Now(), models.Metas{}})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, empty, o)
|
||||
}
|
||||
|
||||
func TestNullLog(t *testing.T) {
|
||||
f, _ := New("zero")
|
||||
o, err := f.Log(models.Log{"jim", []models.Entry{{"title", time.Now(), models.Metas{}}}})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, empty, o)
|
||||
}
|
||||
|
||||
func TestNullLogs(t *testing.T) {
|
||||
f, _ := New("zero")
|
||||
o, err := f.Logs([]models.Log{{"jim", []models.Entry{{"title", time.Now(), models.Metas{}}}}})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, empty, o)
|
||||
}
|
||||
108
formatters/plain.go
Normal file
108
formatters/plain.go
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
package formatters
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"codeberg.org/danjones000/my-log/config"
|
||||
"codeberg.org/danjones000/my-log/models"
|
||||
"codeberg.org/danjones000/my-log/tools"
|
||||
)
|
||||
|
||||
func newPlain(ff config.Formatters) (Formatter, error) {
|
||||
return &PlainText{}, nil
|
||||
}
|
||||
|
||||
type PlainText struct {
|
||||
// config might go here some day
|
||||
}
|
||||
|
||||
func (pt *PlainText) Name() string {
|
||||
return "plain"
|
||||
}
|
||||
|
||||
func (pt *PlainText) Logs(logs []models.Log) (out []byte, err error) {
|
||||
if len(logs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
buff := &bytes.Buffer{}
|
||||
first := true
|
||||
for _, log := range logs {
|
||||
o, _ := pt.Log(log)
|
||||
if !first {
|
||||
buff.WriteByte(10)
|
||||
buff.WriteByte(10)
|
||||
}
|
||||
first = false
|
||||
buff.Write(o)
|
||||
}
|
||||
out = buff.Bytes()
|
||||
return
|
||||
}
|
||||
|
||||
func (pt *PlainText) Log(log models.Log) (out []byte, err error) {
|
||||
if len(log.Entries) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
buff := &bytes.Buffer{}
|
||||
buff.WriteString(log.Name)
|
||||
buff.WriteString("\n#######")
|
||||
written := false
|
||||
for _, e := range log.Entries {
|
||||
bb := pt.entryBuffer(e)
|
||||
if bb.Len() > 0 {
|
||||
buff.WriteByte(10)
|
||||
buff.WriteByte(10)
|
||||
buff.ReadFrom(bb)
|
||||
written = true
|
||||
}
|
||||
}
|
||||
if written {
|
||||
out = buff.Bytes()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (pt *PlainText) entryBuffer(entry models.Entry) *bytes.Buffer {
|
||||
buff := &bytes.Buffer{}
|
||||
buff.WriteString("Title: ")
|
||||
buff.WriteString(entry.Title)
|
||||
buff.WriteByte(10)
|
||||
buff.WriteString("Date: ")
|
||||
buff.WriteString(entry.Date.Format(tools.DateFormat))
|
||||
for _, m := range entry.Fields {
|
||||
bb, err := pt.metaBuffer(m)
|
||||
if (bb.Len() > 0) && (err == nil) {
|
||||
buff.WriteByte(10)
|
||||
buff.ReadFrom(bb)
|
||||
}
|
||||
}
|
||||
|
||||
return buff
|
||||
}
|
||||
|
||||
func (pt *PlainText) Entry(entry models.Entry) ([]byte, error) {
|
||||
buff := pt.entryBuffer(entry)
|
||||
return buff.Bytes(), nil
|
||||
}
|
||||
|
||||
func (pt *PlainText) metaBuffer(meta models.Meta) (*bytes.Buffer, error) {
|
||||
buff := &bytes.Buffer{}
|
||||
buff.WriteString(meta.Key)
|
||||
buff.WriteString(": ")
|
||||
n, err := tools.WriteValue(buff, meta.Value)
|
||||
if n == 0 || err != nil {
|
||||
return &bytes.Buffer{}, err
|
||||
}
|
||||
return buff, nil
|
||||
}
|
||||
|
||||
func (pt *PlainText) Meta(meta models.Meta) (out []byte, err error) {
|
||||
buff, err := pt.metaBuffer(meta)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
out = buff.Bytes()
|
||||
return
|
||||
}
|
||||
140
formatters/plain_test.go
Normal file
140
formatters/plain_test.go
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
package formatters
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"codeberg.org/danjones000/my-log/models"
|
||||
"codeberg.org/danjones000/my-log/tools"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPlainLogs(t *testing.T) {
|
||||
m := []models.Meta{
|
||||
{"foo", "bar"},
|
||||
{"baz", 42},
|
||||
}
|
||||
e := []models.Entry{
|
||||
{Title: "one", Date: time.Now(), Fields: m},
|
||||
{Title: "small", Date: time.Now()},
|
||||
}
|
||||
l := models.Log{"stuff", e}
|
||||
e2 := models.Entry{
|
||||
Title: "three",
|
||||
Date: time.Now(),
|
||||
}
|
||||
l2 := models.Log{"more-stuff", []models.Entry{e2}}
|
||||
logs := []models.Log{l, l2}
|
||||
|
||||
f, err := New("plain")
|
||||
require.NoError(t, err)
|
||||
|
||||
out, err := f.Logs(logs)
|
||||
require.NoError(t, err)
|
||||
|
||||
read := bytes.NewReader(out)
|
||||
scan := bufio.NewScanner(read)
|
||||
|
||||
scan.Scan()
|
||||
line := scan.Text()
|
||||
assert.Equal(t, l.Name, line)
|
||||
|
||||
scan.Scan()
|
||||
line = scan.Text()
|
||||
assert.Equal(t, "#######", line)
|
||||
|
||||
scan.Scan()
|
||||
scan.Scan()
|
||||
line = scan.Text()
|
||||
assert.Equal(t, "Title: "+e[0].Title, line)
|
||||
|
||||
scan.Scan()
|
||||
line = scan.Text()
|
||||
assert.Equal(t, "Date: "+e[0].Date.Format(tools.DateFormat), line)
|
||||
|
||||
scan.Scan()
|
||||
line = scan.Text()
|
||||
assert.Equal(t, "foo: bar", line)
|
||||
|
||||
scan.Scan()
|
||||
line = scan.Text()
|
||||
assert.Equal(t, "baz: 42", line)
|
||||
|
||||
scan.Scan()
|
||||
scan.Scan()
|
||||
line = scan.Text()
|
||||
assert.Equal(t, "Title: "+e[1].Title, line)
|
||||
|
||||
scan.Scan()
|
||||
line = scan.Text()
|
||||
assert.Equal(t, "Date: "+e[1].Date.Format(tools.DateFormat), line)
|
||||
|
||||
scan.Scan()
|
||||
scan.Scan()
|
||||
line = scan.Text()
|
||||
assert.Equal(t, l2.Name, line)
|
||||
|
||||
scan.Scan()
|
||||
line = scan.Text()
|
||||
assert.Equal(t, "#######", line)
|
||||
|
||||
scan.Scan()
|
||||
scan.Scan()
|
||||
line = scan.Text()
|
||||
assert.Equal(t, "Title: "+e2.Title, line)
|
||||
|
||||
scan.Scan()
|
||||
line = scan.Text()
|
||||
assert.Equal(t, "Date: "+e2.Date.Format(tools.DateFormat), line)
|
||||
|
||||
more := scan.Scan()
|
||||
assert.False(t, more)
|
||||
}
|
||||
|
||||
func TestPlainName(t *testing.T) {
|
||||
f, _ := New("plain")
|
||||
assert.Equal(t, "plain", f.Name())
|
||||
}
|
||||
|
||||
func TestPlainLogNone(t *testing.T) {
|
||||
f, _ := New("plain")
|
||||
out, err := f.Logs([]models.Log{})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, out, 0)
|
||||
}
|
||||
|
||||
func TestPlainLogNoEntries(t *testing.T) {
|
||||
f, _ := New("plain")
|
||||
out, err := f.Log(models.Log{Name: "foo"})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, out, 0)
|
||||
}
|
||||
|
||||
func TestPlainMetaEmpty(t *testing.T) {
|
||||
f, _ := New("plain")
|
||||
out, err := f.Meta(models.Meta{"foo", ""})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, out, 0)
|
||||
}
|
||||
|
||||
func TestPlainMetaError(t *testing.T) {
|
||||
f, _ := New("plain")
|
||||
out, err := f.Meta(models.Meta{"foo", make(chan bool)})
|
||||
assert.Error(t, err)
|
||||
assert.Len(t, out, 0)
|
||||
}
|
||||
|
||||
func TestPlainEntry(t *testing.T) {
|
||||
f, _ := New("plain")
|
||||
now := time.Now()
|
||||
out, err := f.Entry(models.Entry{
|
||||
Title: "foo",
|
||||
Date: now,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, fmt.Sprintf("Title: foo\nDate: %s", now.Format(tools.DateFormat)), string(out))
|
||||
}
|
||||
8
go.mod
8
go.mod
|
|
@ -5,10 +5,10 @@ go 1.21.5
|
|||
require (
|
||||
github.com/BurntSushi/toml v1.3.2
|
||||
github.com/caarlos0/env/v10 v10.0.0
|
||||
github.com/markusmobius/go-dateparser v1.2.1
|
||||
github.com/markusmobius/go-dateparser v1.2.3
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/stretchr/testify v1.9.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
@ -25,9 +25,7 @@ require (
|
|||
github.com/tetratelabs/wazero v1.2.1 // indirect
|
||||
github.com/wasilibs/go-re2 v1.3.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20220321173239-a90fa8a75705 // indirect
|
||||
golang.org/x/text v0.10.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace github.com/markusmobius/go-dateparser => github.com/goodevilgenius/go-dateparser v1.2.2
|
||||
|
|
|
|||
12
go.sum
12
go.sum
|
|
@ -8,8 +8,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
|||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3pShXg=
|
||||
github.com/elliotchance/pie/v2 v2.7.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og=
|
||||
github.com/goodevilgenius/go-dateparser v1.2.2 h1:Up9KokPx/h07mesQGAZQg3Xi/8yrDVn1638h3k/lRyk=
|
||||
github.com/goodevilgenius/go-dateparser v1.2.2/go.mod h1:5xYsZ1h7iB3sE1BSu8bkjYpbFST7EU1/AFxcyO3mgYg=
|
||||
github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k=
|
||||
github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ=
|
||||
github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0=
|
||||
|
|
@ -24,6 +22,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/markusmobius/go-dateparser v1.2.3 h1:TvrsIvr5uk+3v6poDjaicnAFJ5IgtFHgLiuMY2Eb7Nw=
|
||||
github.com/markusmobius/go-dateparser v1.2.3/go.mod h1:cMwQRrBUQlK1UI5TIFHEcvpsMbkWrQLXuaPNMFzuYLk=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
|
|
@ -36,8 +36,8 @@ github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
|||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.2.1 h1:J4X2hrGzJvt+wqltuvcSjHQ7ujQxA9gb6PeMs4qlUWs=
|
||||
github.com/tetratelabs/wazero v1.2.1/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ=
|
||||
github.com/wasilibs/go-re2 v1.3.0 h1:LFhBNzoStM3wMie6rN2slD1cuYH2CGiHpvNL3UtcsMw=
|
||||
|
|
@ -46,8 +46,8 @@ github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2e
|
|||
github.com/wasilibs/nottinygc v0.4.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK8jetlB3HIo=
|
||||
golang.org/x/exp v0.0.0-20220321173239-a90fa8a75705 h1:ba9YlqfDGTTQ5aZ2fwOoQ1hf32QySyQkR6ODGDzHlnE=
|
||||
golang.org/x/exp v0.0.0-20220321173239-a90fa8a75705/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
|
||||
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
|||
|
|
@ -16,14 +16,9 @@ import (
|
|||
const DateFormat = tools.DateFormat
|
||||
|
||||
type Entry struct {
|
||||
Title string
|
||||
Date time.Time
|
||||
Fields []Meta
|
||||
skipMissing bool
|
||||
}
|
||||
|
||||
func PartialEntry() Entry {
|
||||
return Entry{skipMissing: true}
|
||||
Title string
|
||||
Date time.Time
|
||||
Fields Metas
|
||||
}
|
||||
|
||||
type metaRes struct {
|
||||
|
|
@ -42,9 +37,9 @@ func (e Entry) getFieldMarshalChan() chan metaRes {
|
|||
defer wg.Done()
|
||||
if m.Key == "json" {
|
||||
if j, ok := m.Value.(json.RawMessage); ok {
|
||||
sub := Entry{skipMissing: true}
|
||||
sub := Metas{}
|
||||
json.Unmarshal(j, &sub)
|
||||
for _, subM := range sub.Fields {
|
||||
for _, subM := range sub {
|
||||
o, er := subM.MarshalText()
|
||||
ch <- metaRes{o, er}
|
||||
}
|
||||
|
|
@ -74,7 +69,7 @@ func (e Entry) MarshalText() ([]byte, error) {
|
|||
}
|
||||
ch := e.getFieldMarshalChan()
|
||||
buff := &bytes.Buffer{}
|
||||
buff.WriteString("\n@begin ")
|
||||
buff.WriteString("@begin ")
|
||||
buff.WriteString(e.Date.Format(DateFormat))
|
||||
buff.WriteString(" - ")
|
||||
buff.WriteString(e.Title)
|
||||
|
|
@ -160,9 +155,9 @@ func (e *Entry) getFieldUnarshalChan(in []byte) chan Meta {
|
|||
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 {
|
||||
ms := Metas{}
|
||||
json.Unmarshal(j, &ms)
|
||||
for _, subM := range ms {
|
||||
ch <- subM
|
||||
}
|
||||
}
|
||||
|
|
@ -191,26 +186,8 @@ func (e Entry) MarshalJSON() ([]byte, error) {
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
for k, v := range e.Fields.Map() {
|
||||
out[k] = v
|
||||
}
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
|
@ -259,16 +236,16 @@ func (e *Entry) UnmarshalJSON(in []byte) error {
|
|||
return newParsingError(err)
|
||||
}
|
||||
title, ok := out["title"].(string)
|
||||
if (!ok || title == "") && !e.skipMissing {
|
||||
if !ok || title == "" {
|
||||
return ErrorMissingTitle
|
||||
}
|
||||
e.Title = title
|
||||
dates, ok := out["date"].(string)
|
||||
if (!ok || dates == "") && !e.skipMissing {
|
||||
if !ok || dates == "" {
|
||||
return ErrorMissingDate
|
||||
}
|
||||
date, err := tools.ParseDate(dates)
|
||||
if err != nil && !e.skipMissing {
|
||||
if err != nil {
|
||||
return newParsingError(err)
|
||||
}
|
||||
e.Date = date
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Type assertions
|
||||
|
|
@ -18,19 +17,6 @@ var _ encoding.TextUnmarshaler = new(Entry)
|
|||
var _ json.Marshaler = Entry{}
|
||||
var _ json.Unmarshaler = new(Entry)
|
||||
|
||||
func TestPartialEntry(t *testing.T) {
|
||||
e := PartialEntry()
|
||||
assert.True(t, e.skipMissing)
|
||||
err := json.Unmarshal([]byte(`{"a":42}`), &e)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", e.Title)
|
||||
assert.Equal(t, time.Time{}, e.Date)
|
||||
require.Len(t, e.Fields, 1)
|
||||
f := e.Fields[0]
|
||||
assert.Equal(t, "a", f.Key)
|
||||
assert.Equal(t, int64(42), f.Value)
|
||||
}
|
||||
|
||||
func TestEntryMarshal(t *testing.T) {
|
||||
when := time.Now()
|
||||
whens := when.Format(DateFormat)
|
||||
|
|
@ -84,19 +70,19 @@ func TestEntryMarshal(t *testing.T) {
|
|||
|
||||
func getEntryMarshalTestRunner(title string, date time.Time, fields []Meta, first string, lines []string, err error) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
en := Entry{title, date, fields, false}
|
||||
en := Entry{title, date, fields}
|
||||
o, er := en.MarshalText()
|
||||
assert.Equal(t, err, er)
|
||||
if first == "" {
|
||||
return
|
||||
}
|
||||
os := string(o)
|
||||
if len(lines) == 0 {
|
||||
assert.Equal(t, "\n"+first, string(o))
|
||||
assert.Equal(t, first, os)
|
||||
return
|
||||
}
|
||||
|
||||
os := string(o)
|
||||
assert.Regexp(t, "^\n"+first, os)
|
||||
assert.Regexp(t, first, os)
|
||||
for _, line := range lines {
|
||||
assert.Regexp(t, "(?m)^"+line, os)
|
||||
}
|
||||
|
|
@ -232,7 +218,7 @@ func TestEntryJsonMarshal(t *testing.T) {
|
|||
|
||||
func getEntryJsonMarshalTestRunner(title string, date time.Time, fields []Meta, out string, err error) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
e := Entry{title, date, fields, false}
|
||||
e := Entry{title, date, fields}
|
||||
o, er := json.Marshal(e)
|
||||
if err == nil {
|
||||
assert.JSONEq(t, out, string(o))
|
||||
|
|
|
|||
|
|
@ -2,12 +2,9 @@ package models
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"codeberg.org/danjones000/my-log/tools"
|
||||
)
|
||||
|
|
@ -25,33 +22,9 @@ func (m Meta) MarshalText() ([]byte, error) {
|
|||
buff.WriteRune('@')
|
||||
buff.WriteString(m.Key)
|
||||
buff.WriteRune(' ')
|
||||
switch v := m.Value.(type) {
|
||||
default:
|
||||
return nil, fmt.Errorf("Unknown type %T", v)
|
||||
case nil:
|
||||
return []byte{}, nil
|
||||
case string:
|
||||
buff.WriteString(v)
|
||||
case int:
|
||||
buff.WriteString(strconv.Itoa(v))
|
||||
case int64:
|
||||
buff.WriteString(strconv.FormatInt(v, 10))
|
||||
case float64:
|
||||
buff.WriteString(strconv.FormatFloat(v, 'f', -1, 64))
|
||||
case json.Number:
|
||||
buff.WriteString(v.String())
|
||||
case json.RawMessage:
|
||||
buff.Write(v)
|
||||
case []byte:
|
||||
buff.Write(v)
|
||||
case byte:
|
||||
buff.WriteByte(v)
|
||||
case rune:
|
||||
buff.WriteString(string(v))
|
||||
case bool:
|
||||
buff.WriteString(strconv.FormatBool(v))
|
||||
case time.Time:
|
||||
buff.WriteString(v.Format(time.RFC3339))
|
||||
n, err := tools.WriteValue(buff, m.Value)
|
||||
if n == 0 || err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
return buff.Bytes(), nil
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import (
|
|||
// Type assertions
|
||||
var _ encoding.TextMarshaler = Meta{}
|
||||
var _ encoding.TextUnmarshaler = new(Meta)
|
||||
var _ json.Marshaler = Metas{}
|
||||
var _ json.Unmarshaler = new(Metas)
|
||||
|
||||
var skipMarshalTest = errors.New("skip marshal")
|
||||
|
||||
|
|
@ -40,7 +42,7 @@ func TestMeta(t *testing.T) {
|
|||
{"byte", "byteme", byte(67), "@byteme C", nil, "C"},
|
||||
{"json-obj", "obj", json.RawMessage(`{"foo":"bar","baz":"quux"}`), `@obj {"foo":"bar","baz":"quux"}`, nil, json.RawMessage(`{"foo":"bar","baz":"quux"}`)},
|
||||
{"json-arr", "arr", json.RawMessage(`["foo",42,"bar", null,"quux", true]`), `@arr ["foo",42,"bar", null,"quux", true]`, nil, json.RawMessage(`["foo",42,"bar", null,"quux", true]`)},
|
||||
{"chan", "nope", make(chan bool), "", errors.New("Unknown type chan bool"), ""},
|
||||
{"chan", "nope", make(chan bool), "", errors.New("Unsupported type chan bool"), ""},
|
||||
{"whitespace-key", "no space", "hi", "", errors.New("whitespace is not allowed in key: no space"), ""},
|
||||
{"empty-mar", "nope", skipMarshalTest, "", nil, ErrorParsing},
|
||||
{"no-key-mar", "nope", skipMarshalTest, "nope", nil, ErrorParsing},
|
||||
|
|
@ -97,3 +99,45 @@ func getMetaTestRunner(key string, value any, out string, err error, newVal any)
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetasJson(t *testing.T) {
|
||||
ms := Metas{{"me", 41}, {"you", false}}
|
||||
exp := `{"me":41,"you":false}`
|
||||
o, err := json.Marshal(ms)
|
||||
assert.NoError(t, err)
|
||||
assert.JSONEq(t, exp, string(o))
|
||||
}
|
||||
|
||||
func TestMetasJsonUnmarshal(t *testing.T) {
|
||||
ms := Metas{}
|
||||
in := `{"me":"cool","you":false}`
|
||||
err := json.Unmarshal([]byte(in), &ms)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, ms, 2)
|
||||
assert.ElementsMatch(t, Metas{
|
||||
{"me", "cool"},
|
||||
{"you", false},
|
||||
}, ms)
|
||||
}
|
||||
|
||||
func TestMetasJsonError(t *testing.T) {
|
||||
ms := Metas{}
|
||||
in := "not json"
|
||||
err := (&ms).UnmarshalJSON([]byte(in))
|
||||
assert.Error(t, err)
|
||||
assert.Len(t, ms, 0)
|
||||
}
|
||||
|
||||
func TestMetasAppend(t *testing.T) {
|
||||
ms := Metas{}
|
||||
ms = ms.Append("foo", 42)
|
||||
assert.Len(t, ms, 1)
|
||||
assert.Equal(t, Meta{"foo", 42}, ms[0])
|
||||
}
|
||||
|
||||
func TestMetasAppendTo(t *testing.T) {
|
||||
ms := &Metas{}
|
||||
ms.AppendTo("foo", 42)
|
||||
assert.Len(t, *ms, 1)
|
||||
assert.Equal(t, Meta{"foo", 42}, (*ms)[0])
|
||||
}
|
||||
|
|
|
|||
71
models/metas.go
Normal file
71
models/metas.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A slice of Meta
|
||||
type Metas []Meta
|
||||
|
||||
// Returns a single map containing all the Meta. Is useful when encoding to JSON
|
||||
func (ms Metas) Map() map[string]any {
|
||||
out := map[string]any{}
|
||||
for _, f := range ms {
|
||||
if _, found := out[f.Key]; found || f.Key == "title" || f.Key == "date" {
|
||||
continue
|
||||
}
|
||||
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 out
|
||||
}
|
||||
|
||||
// Implements json.Marshaler
|
||||
func (ms Metas) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(ms.Map())
|
||||
}
|
||||
|
||||
// Implements json.Unmarshaler
|
||||
func (ms *Metas) UnmarshalJSON(in []byte) error {
|
||||
old := (*ms).Map()
|
||||
out := map[string]any{}
|
||||
err := json.Unmarshal(in, &out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ret := *ms
|
||||
for k, v := range out {
|
||||
if _, found := old[k]; k != "title" && k != "date" && !found {
|
||||
ret = append(ret, Meta{k, v})
|
||||
}
|
||||
}
|
||||
*ms = ret
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns a new Metas with a new Meta appended
|
||||
func (ms Metas) Append(k string, v any) Metas {
|
||||
return append(ms, Meta{k, v})
|
||||
}
|
||||
|
||||
// Appends a new Meta to this Metas
|
||||
func (ms *Metas) AppendTo(k string, v any) {
|
||||
n := (*ms).Append(k, v)
|
||||
*ms = n
|
||||
}
|
||||
|
|
@ -24,11 +24,7 @@ func ParseString(in string) any {
|
|||
if null.MatchString(s) {
|
||||
return nil
|
||||
} else if yesno.MatchString(s) {
|
||||
if yes.MatchString(s) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return yes.MatchString(s)
|
||||
} else if i, err := strconv.Atoi(s); err == nil {
|
||||
return i
|
||||
} else if f, err := strconv.ParseFloat(s, 64); err == nil {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package tools
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
dp "github.com/markusmobius/go-dateparser"
|
||||
|
|
@ -23,6 +24,19 @@ func ParseDate(in string) (t time.Time, err error) {
|
|||
return MaxTime, nil
|
||||
}
|
||||
|
||||
var er error
|
||||
for _, format := range []string{time.RFC3339, DateFormat} {
|
||||
if t, er = time.ParseInLocation(format, in, nil); er == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var ts int64
|
||||
if ts, er = strconv.ParseInt(in, 10, 0); er == nil {
|
||||
t = time.Unix(ts, 0)
|
||||
return
|
||||
}
|
||||
|
||||
conf := dp.Configuration{
|
||||
CurrentTime: time.Now().Local(),
|
||||
ReturnTimeAsPeriod: true,
|
||||
|
|
@ -32,12 +46,11 @@ func ParseDate(in string) (t time.Time, err error) {
|
|||
d, err := dp.Parse(&conf, in)
|
||||
t = d.Time
|
||||
if err != nil {
|
||||
d, err = dp.Parse(&conf, in, DateFormat)
|
||||
t = d.Time
|
||||
return
|
||||
}
|
||||
|
||||
y, mon, day, h, loc := t.Year(), t.Month(), t.Day(), t.Hour(), t.Location()
|
||||
|
||||
switch d.Period {
|
||||
case date.Second:
|
||||
t = t.Truncate(time.Second)
|
||||
|
|
|
|||
|
|
@ -16,14 +16,14 @@ func TestParseDate(t *testing.T) {
|
|||
y, mon, d, h, loc := now.Year(), now.Month(), now.Day(), now.Hour(), now.Location()
|
||||
sec := now.Truncate(time.Second)
|
||||
today := time.Date(y, mon, d, 0, 0, 0, 0, loc)
|
||||
tomorrow := today.Add(day)
|
||||
yesterday := today.Add(-day)
|
||||
tomorrow := time.Date(y, mon, d+1, 0, 0, 0, 0, loc)
|
||||
yesterday := time.Date(y, mon, d-1, 0, 0, 0, 0, loc)
|
||||
twoMin := now.Add(2 * time.Minute).Truncate(time.Minute)
|
||||
twoHour := time.Date(y, mon, d, h+2, 0, 0, 0, loc)
|
||||
firstMonth := time.Date(y, mon, 1, 0, 0, 0, 0, loc)
|
||||
firstYear := time.Date(y, 1, 1, 0, 0, 0, 0, loc)
|
||||
exact := "2075-02-12T12:13:54.536-02:00"
|
||||
exactd, _ := time.ParseInLocation(time.RFC3339, exact, time.FixedZone("UTC-02:00", -7200))
|
||||
exactd, _ := time.ParseInLocation(time.RFC3339, exact, nil)
|
||||
var ts int64 = 1708876012
|
||||
tsd := time.Unix(ts, 0)
|
||||
ent := "February 25, 2024 at 04:00:13AM +0230"
|
||||
|
|
|
|||
56
tools/write_buffer.go
Normal file
56
tools/write_buffer.go
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
package tools
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func WriteValue(buff *bytes.Buffer, val any) (n int, err error) {
|
||||
switch v := val.(type) {
|
||||
default:
|
||||
err = fmt.Errorf("Unsupported type %T", v)
|
||||
case nil:
|
||||
return
|
||||
case []any:
|
||||
var o []byte
|
||||
o, err = json.Marshal(v)
|
||||
if err == nil {
|
||||
return buff.Write(o)
|
||||
}
|
||||
case map[string]any:
|
||||
var o []byte
|
||||
o, err = json.Marshal(v)
|
||||
if err == nil {
|
||||
return buff.Write(o)
|
||||
}
|
||||
case string:
|
||||
return buff.WriteString(v)
|
||||
case int:
|
||||
return buff.WriteString(strconv.Itoa(v))
|
||||
case int64:
|
||||
return buff.WriteString(strconv.FormatInt(v, 10))
|
||||
case float64:
|
||||
return buff.WriteString(strconv.FormatFloat(v, 'f', -1, 64))
|
||||
case json.Number:
|
||||
return buff.WriteString(v.String())
|
||||
case json.RawMessage:
|
||||
return buff.Write(v)
|
||||
case []byte:
|
||||
return buff.Write(v)
|
||||
case byte:
|
||||
err = buff.WriteByte(v)
|
||||
if err == nil {
|
||||
n = 1
|
||||
}
|
||||
case rune:
|
||||
return buff.WriteString(string(v))
|
||||
case bool:
|
||||
return buff.WriteString(strconv.FormatBool(v))
|
||||
case time.Time:
|
||||
return buff.WriteString(v.Format(time.RFC3339))
|
||||
}
|
||||
return
|
||||
}
|
||||
51
tools/write_buffer_test.go
Normal file
51
tools/write_buffer_test.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package tools
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWriteBuffer(t *testing.T) {
|
||||
when := time.Now()
|
||||
tests := []struct {
|
||||
name string
|
||||
value any
|
||||
out string
|
||||
err error
|
||||
}{
|
||||
{"nil", nil, "", nil},
|
||||
{"string", "hi", "hi", nil},
|
||||
{"bytes", []byte{104, 105}, "hi", nil},
|
||||
{"byte", byte(104), "h", nil},
|
||||
{"rune", 'h', "h", nil},
|
||||
{"int", 42, "42", nil},
|
||||
{"int64", int64(42), "42", nil},
|
||||
{"float", 42.13, "42.13", nil},
|
||||
{"bool", false, "false", nil},
|
||||
{"json.Number", json.Number("42.13"), "42.13", nil},
|
||||
{"json.RawMessage", json.RawMessage("{}"), "{}", nil},
|
||||
{"time", when, when.Format(time.RFC3339), nil},
|
||||
{"slice", []any{1, 2, "foo"}, `[1,2,"foo"]`, nil},
|
||||
{"map", map[string]any{"baz": 42, "foo": "bar"}, `{"baz":42,"foo":"bar"}`, nil},
|
||||
{"struct", struct{}{}, "", errors.New("Unsupported type struct {}")},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, getWriteTestRunner(tt.value, tt.out, tt.err))
|
||||
}
|
||||
}
|
||||
|
||||
func getWriteTestRunner(value any, out string, err error) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
buff := &bytes.Buffer{}
|
||||
n, er := WriteValue(buff, value)
|
||||
assert.Equal(t, len(out), n)
|
||||
assert.Equal(t, err, er)
|
||||
assert.Equal(t, out, buff.String())
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue