update
This commit is contained in:
parent
f103d12a03
commit
f81ce78e6f
|
@ -3,6 +3,10 @@ name = "psychonaut_journal_cli"
|
|||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "journal-cli"
|
||||
path = "journal_cli/src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
journal = { path = "./journal" }
|
||||
chrono = { version = "0.4.38", features = ["serde"] }
|
||||
|
|
1
gojq-extended/.gitignore
vendored
1
gojq-extended/.gitignore
vendored
|
@ -1 +0,0 @@
|
|||
gojq-extended
|
|
@ -1,22 +0,0 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2024 chaos
|
||||
Copyright (c) 2019-2024 itchyny
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -1,21 +0,0 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019-2024 itchyny
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -1,476 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
|
||||
"github.com/itchyny/gojq"
|
||||
)
|
||||
|
||||
const name = "gojq"
|
||||
|
||||
const version = "0.12.16"
|
||||
|
||||
var revision = "HEAD"
|
||||
|
||||
const (
|
||||
exitCodeOK = iota
|
||||
exitCodeFalsyErr
|
||||
exitCodeFlagParseErr
|
||||
exitCodeCompileErr
|
||||
exitCodeNoValueErr
|
||||
exitCodeDefaultErr
|
||||
)
|
||||
|
||||
type cli struct {
|
||||
inStream io.Reader
|
||||
outStream io.Writer
|
||||
errStream io.Writer
|
||||
|
||||
outputRaw bool
|
||||
outputRaw0 bool
|
||||
outputJoin bool
|
||||
outputCompact bool
|
||||
outputIndent *int
|
||||
outputTab bool
|
||||
inputRaw bool
|
||||
inputStream bool
|
||||
inputSlurp bool
|
||||
|
||||
argnames []string
|
||||
argvalues []any
|
||||
|
||||
exitCodeError error
|
||||
}
|
||||
|
||||
type flagopts struct {
|
||||
OutputRaw bool `short:"r" long:"raw-output" description:"output raw strings"`
|
||||
OutputRaw0 bool `long:"raw-output0" description:"implies -r with NUL character delimiter"`
|
||||
OutputJoin bool `short:"j" long:"join-output" description:"implies -r with no newline delimiter"`
|
||||
OutputCompact bool `short:"c" long:"compact-output" description:"output without pretty-printing"`
|
||||
OutputIndent *int `long:"indent" description:"number of spaces for indentation"`
|
||||
OutputTab bool `long:"tab" description:"use tabs for indentation"`
|
||||
OutputColor bool `short:"C" long:"color-output" description:"output with colors even if piped"`
|
||||
OutputMono bool `short:"M" long:"monochrome-output" description:"output without colors"`
|
||||
InputNull bool `short:"n" long:"null-input" description:"use null as input value"`
|
||||
InputRaw bool `short:"R" long:"raw-input" description:"read input as raw strings"`
|
||||
InputStream bool `long:"stream" description:"parse input in stream fashion"`
|
||||
InputSlurp bool `short:"s" long:"slurp" description:"read all inputs into an array"`
|
||||
FromFile bool `short:"f" long:"from-file" description:"load query from file"`
|
||||
ModulePaths []string `short:"L" description:"directory to search modules from"`
|
||||
Arg map[string]string `long:"arg" description:"set a string value to a variable"`
|
||||
ArgJSON map[string]string `long:"argjson" description:"set a JSON value to a variable"`
|
||||
SlurpFile map[string]string `long:"slurpfile" description:"set the JSON contents of a file to a variable"`
|
||||
RawFile map[string]string `long:"rawfile" description:"set the contents of a file to a variable"`
|
||||
Args []any `long:"args" positional:"" description:"consume remaining arguments as positional string values"`
|
||||
JSONArgs []any `long:"jsonargs" positional:"" description:"consume remaining arguments as positional JSON values"`
|
||||
ExitStatus bool `short:"e" long:"exit-status" description:"exit 1 when the last value is false or null"`
|
||||
Version bool `short:"v" long:"version" description:"display version information"`
|
||||
Help bool `short:"h" long:"help" description:"display this help information"`
|
||||
}
|
||||
|
||||
var addDefaultModulePaths = true
|
||||
|
||||
func (cli *cli) run(args []string) int {
|
||||
if err := cli.runInternal(args); err != nil {
|
||||
if _, ok := err.(interface{ isEmptyError() }); !ok {
|
||||
fmt.Fprintf(cli.errStream, "%s: %s\n", name, err)
|
||||
}
|
||||
if err, ok := err.(interface{ ExitCode() int }); ok {
|
||||
return err.ExitCode()
|
||||
}
|
||||
return exitCodeDefaultErr
|
||||
}
|
||||
return exitCodeOK
|
||||
}
|
||||
|
||||
func (cli *cli) runInternal(args []string) (err error) {
|
||||
var opts flagopts
|
||||
args, err = parseFlags(args, &opts)
|
||||
if err != nil {
|
||||
return &flagParseError{err}
|
||||
}
|
||||
if opts.Help {
|
||||
fmt.Fprintf(cli.outStream, `%[1]s - Go implementation of jq
|
||||
|
||||
Version: %s (rev: %s/%s)
|
||||
|
||||
Synopsis:
|
||||
%% echo '{"foo": 128}' | %[1]s '.foo'
|
||||
|
||||
Usage:
|
||||
%[1]s [OPTIONS]
|
||||
|
||||
`,
|
||||
name, version, revision, runtime.Version())
|
||||
fmt.Fprintln(cli.outStream, formatFlags(&opts))
|
||||
return nil
|
||||
}
|
||||
if opts.Version {
|
||||
fmt.Fprintf(cli.outStream, "%s %s (rev: %s/%s)\n", name, version, revision, runtime.Version())
|
||||
return nil
|
||||
}
|
||||
cli.outputRaw, cli.outputRaw0, cli.outputJoin,
|
||||
cli.outputCompact, cli.outputIndent, cli.outputTab =
|
||||
opts.OutputRaw, opts.OutputRaw0, opts.OutputJoin,
|
||||
opts.OutputCompact, opts.OutputIndent, opts.OutputTab
|
||||
defer func(x bool) { noColor = x }(noColor)
|
||||
if opts.OutputColor || opts.OutputMono {
|
||||
noColor = opts.OutputMono
|
||||
} else if os.Getenv("NO_COLOR") != "" || os.Getenv("TERM") == "dumb" {
|
||||
noColor = true
|
||||
} else {
|
||||
f, ok := cli.outStream.(interface{ Fd() uintptr })
|
||||
noColor = !(ok && (isatty.IsTerminal(f.Fd()) || isatty.IsCygwinTerminal(f.Fd())))
|
||||
}
|
||||
if !noColor {
|
||||
if colors := os.Getenv("GOJQ_COLORS"); colors != "" {
|
||||
if err := setColors(colors); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if i := cli.outputIndent; i != nil {
|
||||
if *i > 9 {
|
||||
return fmt.Errorf("too many indentation count: %d", *i)
|
||||
} else if *i < 0 {
|
||||
return fmt.Errorf("negative indentation count: %d", *i)
|
||||
}
|
||||
}
|
||||
|
||||
cli.inputRaw, cli.inputStream, cli.inputSlurp =
|
||||
opts.InputRaw, opts.InputStream, opts.InputSlurp
|
||||
for k, v := range opts.Arg {
|
||||
cli.argnames = append(cli.argnames, "$"+k)
|
||||
cli.argvalues = append(cli.argvalues, v)
|
||||
}
|
||||
for k, v := range opts.ArgJSON {
|
||||
val, _ := newJSONInputIter(strings.NewReader(v), "$"+k).Next()
|
||||
if err, ok := val.(error); ok {
|
||||
return err
|
||||
}
|
||||
cli.argnames = append(cli.argnames, "$"+k)
|
||||
cli.argvalues = append(cli.argvalues, val)
|
||||
}
|
||||
for k, v := range opts.SlurpFile {
|
||||
val, err := slurpFile(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cli.argnames = append(cli.argnames, "$"+k)
|
||||
cli.argvalues = append(cli.argvalues, val)
|
||||
}
|
||||
for k, v := range opts.RawFile {
|
||||
val, err := os.ReadFile(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cli.argnames = append(cli.argnames, "$"+k)
|
||||
cli.argvalues = append(cli.argvalues, string(val))
|
||||
}
|
||||
named := make(map[string]any, len(cli.argnames))
|
||||
for i, name := range cli.argnames {
|
||||
named[name[1:]] = cli.argvalues[i]
|
||||
}
|
||||
positional := opts.Args
|
||||
for i, v := range opts.JSONArgs {
|
||||
if v != nil {
|
||||
val, _ := newJSONInputIter(strings.NewReader(v.(string)), "--jsonargs").Next()
|
||||
if err, ok := val.(error); ok {
|
||||
return err
|
||||
}
|
||||
if i < len(positional) {
|
||||
positional[i] = val
|
||||
} else {
|
||||
positional = append(positional, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
cli.argnames = append(cli.argnames, "$ARGS")
|
||||
cli.argvalues = append(cli.argvalues, map[string]any{
|
||||
"named": named,
|
||||
"positional": positional,
|
||||
})
|
||||
var arg, fname string
|
||||
if opts.FromFile {
|
||||
if len(args) == 0 {
|
||||
return errors.New("expected a query file for flag `-f'")
|
||||
}
|
||||
src, err := os.ReadFile(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
arg, args, fname = string(src), args[1:], args[0]
|
||||
} else if len(args) == 0 {
|
||||
arg = "."
|
||||
} else {
|
||||
arg, args, fname = strings.TrimSpace(args[0]), args[1:], "<arg>"
|
||||
}
|
||||
if opts.ExitStatus {
|
||||
cli.exitCodeError = &exitCodeError{exitCodeNoValueErr}
|
||||
defer func() {
|
||||
if _, ok := err.(interface{ ExitCode() int }); !ok {
|
||||
err = cli.exitCodeError
|
||||
}
|
||||
}()
|
||||
}
|
||||
query, err := gojq.Parse(arg)
|
||||
if err != nil {
|
||||
return &queryParseError{fname, arg, err}
|
||||
}
|
||||
modulePaths := opts.ModulePaths
|
||||
if len(modulePaths) == 0 && addDefaultModulePaths {
|
||||
modulePaths = []string{"~/.jq", "$ORIGIN/../lib/gojq", "$ORIGIN/../lib"}
|
||||
}
|
||||
iter := cli.createInputIter(args)
|
||||
defer iter.Close()
|
||||
code, err := gojq.Compile(query,
|
||||
gojq.WithModuleLoader(gojq.NewModuleLoader(modulePaths)),
|
||||
gojq.WithEnvironLoader(os.Environ),
|
||||
gojq.WithVariables(cli.argnames),
|
||||
gojq.WithFunction("debug", 0, 0,
|
||||
func(errStream io.Writer) func(any, []any) any {
|
||||
indent := 2
|
||||
if cli.outputCompact {
|
||||
indent = 0
|
||||
} else if cli.outputTab {
|
||||
indent = 1
|
||||
} else if i := cli.outputIndent; i != nil {
|
||||
indent = *i
|
||||
}
|
||||
|
||||
return func(v any, _ []any) any {
|
||||
if err := newEncoder(false, indent).
|
||||
marshal([]any{"DEBUG:", v}, cli.errStream); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := cli.errStream.Write([]byte{'\n'}); err != nil {
|
||||
return err
|
||||
}
|
||||
return v
|
||||
}
|
||||
}(cli.errStream),
|
||||
),
|
||||
|
||||
|
||||
gojq.WithFunction("stderr", 0, 0,
|
||||
func(errStream io.Writer) func(any, []any) any {
|
||||
return func(v any, _ []any) any {
|
||||
if err := (&rawMarshaler{m: newEncoder(false, 0)}).
|
||||
marshal(v, cli.errStream); err != nil {
|
||||
return err
|
||||
}
|
||||
return v
|
||||
}
|
||||
}(cli.errStream),
|
||||
),
|
||||
gojq.WithFunction("_readFile", 1, 1,
|
||||
func(_input any, args []any) any {
|
||||
filename := args[0].(string)
|
||||
|
||||
info, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("file not found")
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return fmt.Errorf("file is a directory")
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("file could not be read")
|
||||
}
|
||||
return string(content)
|
||||
},
|
||||
),
|
||||
gojq.WithFunction("_writeFileString", 1, 1,
|
||||
func(input any, args []any) any {
|
||||
filename := args[0].(string)
|
||||
content := ""
|
||||
|
||||
switch input := input.(type) {
|
||||
case string:
|
||||
content = input
|
||||
default:
|
||||
return fmt.Errorf("invalid type passed to _writeFileString")
|
||||
}
|
||||
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = file.Write([]byte(content))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
),
|
||||
gojq.WithFunction("input_filename", 0, 0,
|
||||
func(iter inputIter) func(any, []any) any {
|
||||
return func(any, []any) any {
|
||||
if fname := iter.Name(); fname != "" && (len(args) > 0 || !opts.InputNull) {
|
||||
return fname
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}(iter),
|
||||
),
|
||||
gojq.WithInputIter(iter),
|
||||
)
|
||||
if err != nil {
|
||||
if err, ok := err.(interface {
|
||||
QueryParseError() (string, string, error)
|
||||
}); ok {
|
||||
name, query, err := err.QueryParseError()
|
||||
return &queryParseError{name, query, err}
|
||||
}
|
||||
if err, ok := err.(interface {
|
||||
JSONParseError() (string, string, error)
|
||||
}); ok {
|
||||
fname, contents, err := err.JSONParseError()
|
||||
return &compileError{&jsonParseError{fname, contents, 0, err}}
|
||||
}
|
||||
return &compileError{err}
|
||||
}
|
||||
if opts.InputNull {
|
||||
iter = newNullInputIter()
|
||||
}
|
||||
return cli.process(iter, code)
|
||||
}
|
||||
|
||||
func slurpFile(name string) (any, error) {
|
||||
iter := newSlurpInputIter(
|
||||
newFilesInputIter(newJSONInputIter, []string{name}, nil),
|
||||
)
|
||||
defer iter.Close()
|
||||
val, _ := iter.Next()
|
||||
if err, ok := val.(error); ok {
|
||||
return nil, err
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func (cli *cli) createInputIter(args []string) (iter inputIter) {
|
||||
var newIter func(io.Reader, string) inputIter
|
||||
switch {
|
||||
case cli.inputRaw:
|
||||
if cli.inputSlurp {
|
||||
newIter = newReadAllIter
|
||||
} else {
|
||||
newIter = newRawInputIter
|
||||
}
|
||||
case cli.inputStream:
|
||||
newIter = newStreamInputIter
|
||||
default:
|
||||
newIter = newJSONInputIter
|
||||
}
|
||||
if cli.inputSlurp {
|
||||
defer func() {
|
||||
if cli.inputRaw {
|
||||
iter = newSlurpRawInputIter(iter)
|
||||
} else {
|
||||
iter = newSlurpInputIter(iter)
|
||||
}
|
||||
}()
|
||||
}
|
||||
if len(args) == 0 {
|
||||
return newIter(cli.inStream, "<stdin>")
|
||||
}
|
||||
return newFilesInputIter(newIter, args, cli.inStream)
|
||||
}
|
||||
|
||||
func (cli *cli) process(iter inputIter, code *gojq.Code) error {
|
||||
var err error
|
||||
for {
|
||||
v, ok := iter.Next()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if e, ok := v.(error); ok {
|
||||
fmt.Fprintf(cli.errStream, "%s: %s\n", name, e)
|
||||
err = e
|
||||
continue
|
||||
}
|
||||
if e := cli.printValues(code.Run(v, cli.argvalues...)); e != nil {
|
||||
if e, ok := e.(*gojq.HaltError); ok {
|
||||
if v := e.Value(); v != nil {
|
||||
if str, ok := v.(string); ok {
|
||||
cli.errStream.Write([]byte(str))
|
||||
} else {
|
||||
bs, _ := gojq.Marshal(v)
|
||||
cli.errStream.Write(bs)
|
||||
cli.errStream.Write([]byte{'\n'})
|
||||
}
|
||||
}
|
||||
err = e
|
||||
break
|
||||
}
|
||||
fmt.Fprintf(cli.errStream, "%s: %s\n", name, e)
|
||||
err = e
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return &emptyError{err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *cli) printValues(iter gojq.Iter) error {
|
||||
m := cli.createMarshaler()
|
||||
for {
|
||||
v, ok := iter.Next()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if err, ok := v.(error); ok {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.marshal(v, cli.outStream); err != nil {
|
||||
return err
|
||||
}
|
||||
if cli.exitCodeError != nil {
|
||||
if v == nil || v == false {
|
||||
cli.exitCodeError = &exitCodeError{exitCodeFalsyErr}
|
||||
} else {
|
||||
cli.exitCodeError = &exitCodeError{exitCodeOK}
|
||||
}
|
||||
}
|
||||
if cli.outputRaw0 {
|
||||
cli.outStream.Write([]byte{'\x00'})
|
||||
} else if !cli.outputJoin {
|
||||
cli.outStream.Write([]byte{'\n'})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *cli) createMarshaler() marshaler {
|
||||
indent := 2
|
||||
if cli.outputCompact {
|
||||
indent = 0
|
||||
} else if cli.outputTab {
|
||||
indent = 1
|
||||
} else if i := cli.outputIndent; i != nil {
|
||||
indent = *i
|
||||
}
|
||||
f := newEncoder(cli.outputTab, indent)
|
||||
if cli.outputRaw || cli.outputRaw0 || cli.outputJoin {
|
||||
return &rawMarshaler{f, cli.outputRaw0}
|
||||
}
|
||||
return f
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var noColor bool
|
||||
|
||||
func newColor(c string) []byte {
|
||||
return []byte("\x1b[" + c + "m")
|
||||
}
|
||||
|
||||
func setColor(buf *bytes.Buffer, color []byte) {
|
||||
if !noColor {
|
||||
buf.Write(color)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
resetColor = newColor("0") // Reset
|
||||
nullColor = newColor("90") // Bright black
|
||||
falseColor = newColor("33") // Yellow
|
||||
trueColor = newColor("33") // Yellow
|
||||
numberColor = newColor("36") // Cyan
|
||||
stringColor = newColor("32") // Green
|
||||
objectKeyColor = newColor("34;1") // Bold Blue
|
||||
arrayColor = []byte(nil) // No color
|
||||
objectColor = []byte(nil) // No color
|
||||
)
|
||||
|
||||
func validColor(x string) bool {
|
||||
var num bool
|
||||
for _, c := range x {
|
||||
if '0' <= c && c <= '9' {
|
||||
num = true
|
||||
} else if c == ';' && num {
|
||||
num = false
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return num
|
||||
}
|
||||
|
||||
func setColors(colors string) error {
|
||||
var color string
|
||||
for _, target := range []*[]byte{
|
||||
&nullColor, &falseColor, &trueColor, &numberColor,
|
||||
&stringColor, &objectKeyColor, &arrayColor, &objectColor,
|
||||
} {
|
||||
color, colors, _ = strings.Cut(colors, ":")
|
||||
if color != "" {
|
||||
if !validColor(color) {
|
||||
return fmt.Errorf("invalid color: %q", color)
|
||||
}
|
||||
*target = newColor(color)
|
||||
} else {
|
||||
*target = nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,267 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"math/big"
|
||||
"sort"
|
||||
"strconv"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type encoder struct {
|
||||
out io.Writer
|
||||
w *bytes.Buffer
|
||||
tab bool
|
||||
indent int
|
||||
depth int
|
||||
buf [64]byte
|
||||
}
|
||||
|
||||
func newEncoder(tab bool, indent int) *encoder {
|
||||
// reuse the buffer in multiple calls of marshal
|
||||
return &encoder{w: new(bytes.Buffer), tab: tab, indent: indent}
|
||||
}
|
||||
|
||||
func (e *encoder) flush() error {
|
||||
_, err := e.out.Write(e.w.Bytes())
|
||||
e.w.Reset()
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *encoder) marshal(v any, w io.Writer) error {
|
||||
e.out = w
|
||||
err := e.encode(v)
|
||||
if ferr := e.flush(); ferr != nil && err == nil {
|
||||
err = ferr
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *encoder) encode(v any) error {
|
||||
switch v := v.(type) {
|
||||
case nil:
|
||||
e.write([]byte("null"), nullColor)
|
||||
case bool:
|
||||
if v {
|
||||
e.write([]byte("true"), trueColor)
|
||||
} else {
|
||||
e.write([]byte("false"), falseColor)
|
||||
}
|
||||
case int:
|
||||
e.write(strconv.AppendInt(e.buf[:0], int64(v), 10), numberColor)
|
||||
case float64:
|
||||
e.encodeFloat64(v)
|
||||
case *big.Int:
|
||||
e.write(v.Append(e.buf[:0], 10), numberColor)
|
||||
case string:
|
||||
e.encodeString(v, stringColor)
|
||||
case []any:
|
||||
if err := e.encodeArray(v); err != nil {
|
||||
return err
|
||||
}
|
||||
case map[string]any:
|
||||
if err := e.encodeObject(v); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid type: %[1]T (%[1]v)", v))
|
||||
}
|
||||
if e.w.Len() > 8*1024 {
|
||||
return e.flush()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ref: floatEncoder in encoding/json
|
||||
func (e *encoder) encodeFloat64(f float64) {
|
||||
if math.IsNaN(f) {
|
||||
e.write([]byte("null"), nullColor)
|
||||
return
|
||||
}
|
||||
f = min(max(f, -math.MaxFloat64), math.MaxFloat64)
|
||||
format := byte('f')
|
||||
if x := math.Abs(f); x != 0 && x < 1e-6 || x >= 1e21 {
|
||||
format = 'e'
|
||||
}
|
||||
buf := strconv.AppendFloat(e.buf[:0], f, format, -1, 64)
|
||||
if format == 'e' {
|
||||
// clean up e-09 to e-9
|
||||
if n := len(buf); n >= 4 && buf[n-4] == 'e' && buf[n-3] == '-' && buf[n-2] == '0' {
|
||||
buf[n-2] = buf[n-1]
|
||||
buf = buf[:n-1]
|
||||
}
|
||||
}
|
||||
e.write(buf, numberColor)
|
||||
}
|
||||
|
||||
// ref: encodeState#string in encoding/json
|
||||
func (e *encoder) encodeString(s string, color []byte) {
|
||||
if color != nil {
|
||||
setColor(e.w, color)
|
||||
}
|
||||
e.w.WriteByte('"')
|
||||
start := 0
|
||||
for i := 0; i < len(s); {
|
||||
if b := s[i]; b < utf8.RuneSelf {
|
||||
if ' ' <= b && b <= '~' && b != '"' && b != '\\' {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if start < i {
|
||||
e.w.WriteString(s[start:i])
|
||||
}
|
||||
switch b {
|
||||
case '"':
|
||||
e.w.WriteString(`\"`)
|
||||
case '\\':
|
||||
e.w.WriteString(`\\`)
|
||||
case '\b':
|
||||
e.w.WriteString(`\b`)
|
||||
case '\f':
|
||||
e.w.WriteString(`\f`)
|
||||
case '\n':
|
||||
e.w.WriteString(`\n`)
|
||||
case '\r':
|
||||
e.w.WriteString(`\r`)
|
||||
case '\t':
|
||||
e.w.WriteString(`\t`)
|
||||
default:
|
||||
const hex = "0123456789abcdef"
|
||||
e.w.WriteString(`\u00`)
|
||||
e.w.WriteByte(hex[b>>4])
|
||||
e.w.WriteByte(hex[b&0xF])
|
||||
}
|
||||
i++
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
c, size := utf8.DecodeRuneInString(s[i:])
|
||||
if c == utf8.RuneError && size == 1 {
|
||||
if start < i {
|
||||
e.w.WriteString(s[start:i])
|
||||
}
|
||||
e.w.WriteString(`\ufffd`)
|
||||
i += size
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
i += size
|
||||
}
|
||||
if start < len(s) {
|
||||
e.w.WriteString(s[start:])
|
||||
}
|
||||
e.w.WriteByte('"')
|
||||
if color != nil {
|
||||
setColor(e.w, resetColor)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *encoder) encodeArray(vs []any) error {
|
||||
e.writeByte('[', arrayColor)
|
||||
e.depth += e.indent
|
||||
for i, v := range vs {
|
||||
if i > 0 {
|
||||
e.writeByte(',', arrayColor)
|
||||
}
|
||||
if e.indent != 0 {
|
||||
e.writeIndent()
|
||||
}
|
||||
if err := e.encode(v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
e.depth -= e.indent
|
||||
if len(vs) > 0 && e.indent != 0 {
|
||||
e.writeIndent()
|
||||
}
|
||||
e.writeByte(']', arrayColor)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *encoder) encodeObject(vs map[string]any) error {
|
||||
e.writeByte('{', objectColor)
|
||||
e.depth += e.indent
|
||||
type keyVal struct {
|
||||
key string
|
||||
val any
|
||||
}
|
||||
kvs := make([]keyVal, len(vs))
|
||||
var i int
|
||||
for k, v := range vs {
|
||||
kvs[i] = keyVal{k, v}
|
||||
i++
|
||||
}
|
||||
sort.Slice(kvs, func(i, j int) bool {
|
||||
return kvs[i].key < kvs[j].key
|
||||
})
|
||||
for i, kv := range kvs {
|
||||
if i > 0 {
|
||||
e.writeByte(',', objectColor)
|
||||
}
|
||||
if e.indent != 0 {
|
||||
e.writeIndent()
|
||||
}
|
||||
e.encodeString(kv.key, objectKeyColor)
|
||||
e.writeByte(':', objectColor)
|
||||
if e.indent != 0 {
|
||||
e.w.WriteByte(' ')
|
||||
}
|
||||
if err := e.encode(kv.val); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
e.depth -= e.indent
|
||||
if len(vs) > 0 && e.indent != 0 {
|
||||
e.writeIndent()
|
||||
}
|
||||
e.writeByte('}', objectColor)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *encoder) writeIndent() {
|
||||
e.w.WriteByte('\n')
|
||||
if n := e.depth; n > 0 {
|
||||
if e.tab {
|
||||
e.writeIndentInternal(n, "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t")
|
||||
} else {
|
||||
e.writeIndentInternal(n, " ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *encoder) writeIndentInternal(n int, spaces string) {
|
||||
if l := len(spaces); n <= l {
|
||||
e.w.WriteString(spaces[:n])
|
||||
} else {
|
||||
e.w.WriteString(spaces)
|
||||
for n -= l; n > 0; n, l = n-l, l*2 {
|
||||
if n < l {
|
||||
l = n
|
||||
}
|
||||
e.w.Write(e.w.Bytes()[e.w.Len()-l:])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *encoder) writeByte(b byte, color []byte) {
|
||||
if color == nil {
|
||||
e.w.WriteByte(b)
|
||||
} else {
|
||||
setColor(e.w, color)
|
||||
e.w.WriteByte(b)
|
||||
setColor(e.w, resetColor)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *encoder) write(bs []byte, color []byte) {
|
||||
if color == nil {
|
||||
e.w.Write(bs)
|
||||
} else {
|
||||
setColor(e.w, color)
|
||||
e.w.Write(bs)
|
||||
setColor(e.w, resetColor)
|
||||
}
|
||||
}
|
|
@ -1,225 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
|
||||
"github.com/itchyny/gojq"
|
||||
)
|
||||
|
||||
type emptyError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (*emptyError) Error() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (*emptyError) isEmptyError() {}
|
||||
|
||||
func (err *emptyError) ExitCode() int {
|
||||
if err, ok := err.err.(interface{ ExitCode() int }); ok {
|
||||
return err.ExitCode()
|
||||
}
|
||||
return exitCodeDefaultErr
|
||||
}
|
||||
|
||||
type exitCodeError struct {
|
||||
code int
|
||||
}
|
||||
|
||||
func (err *exitCodeError) Error() string {
|
||||
return "exit code: " + strconv.Itoa(err.code)
|
||||
}
|
||||
|
||||
func (*exitCodeError) isEmptyError() {}
|
||||
|
||||
func (err *exitCodeError) ExitCode() int {
|
||||
return err.code
|
||||
}
|
||||
|
||||
type flagParseError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (err *flagParseError) Error() string {
|
||||
return err.err.Error()
|
||||
}
|
||||
|
||||
func (*flagParseError) ExitCode() int {
|
||||
return exitCodeFlagParseErr
|
||||
}
|
||||
|
||||
type compileError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (err *compileError) Error() string {
|
||||
return "compile error: " + err.err.Error()
|
||||
}
|
||||
|
||||
func (*compileError) ExitCode() int {
|
||||
return exitCodeCompileErr
|
||||
}
|
||||
|
||||
type queryParseError struct {
|
||||
fname, contents string
|
||||
err error
|
||||
}
|
||||
|
||||
func (err *queryParseError) Error() string {
|
||||
var offset int
|
||||
var e *gojq.ParseError
|
||||
if errors.As(err.err, &e) {
|
||||
offset = e.Offset - len(e.Token) + 1
|
||||
}
|
||||
linestr, line, column := getLineByOffset(err.contents, offset)
|
||||
if err.fname != "<arg>" || containsNewline(err.contents) {
|
||||
return fmt.Sprintf("invalid query: %s:%d\n%s %s",
|
||||
err.fname, line, formatLineInfo(linestr, line, column), err.err)
|
||||
}
|
||||
return fmt.Sprintf("invalid query: %s\n %s\n %*c %s",
|
||||
err.contents, linestr, column+1, '^', err.err)
|
||||
}
|
||||
|
||||
func (*queryParseError) ExitCode() int {
|
||||
return exitCodeCompileErr
|
||||
}
|
||||
|
||||
type jsonParseError struct {
|
||||
fname, contents string
|
||||
line int
|
||||
err error
|
||||
}
|
||||
|
||||
func (err *jsonParseError) Error() string {
|
||||
var offset int
|
||||
if err.err == io.ErrUnexpectedEOF {
|
||||
offset = len(err.contents) + 1
|
||||
} else if e, ok := err.err.(*json.SyntaxError); ok {
|
||||
offset = int(e.Offset)
|
||||
}
|
||||
linestr, line, column := getLineByOffset(err.contents, offset)
|
||||
if line += err.line; line > 1 {
|
||||
return fmt.Sprintf("invalid json: %s:%d\n%s %s",
|
||||
err.fname, line, formatLineInfo(linestr, line, column), err.err)
|
||||
}
|
||||
return fmt.Sprintf("invalid json: %s\n %s\n %*c %s",
|
||||
err.fname, linestr, column+1, '^', err.err)
|
||||
}
|
||||
|
||||
func getLineByOffset(str string, offset int) (linestr string, line, column int) {
|
||||
ss := &stringScanner{str, 0}
|
||||
for {
|
||||
str, start, ok := ss.next()
|
||||
if !ok {
|
||||
offset -= start
|
||||
break
|
||||
}
|
||||
line++
|
||||
linestr = str
|
||||
if ss.offset >= offset {
|
||||
offset -= start
|
||||
break
|
||||
}
|
||||
}
|
||||
offset = min(max(offset-1, 0), len(linestr))
|
||||
if offset > 48 {
|
||||
skip := len(trimLastInvalidRune(linestr[:offset-48]))
|
||||
linestr = linestr[skip:]
|
||||
offset -= skip
|
||||
}
|
||||
linestr = trimLastInvalidRune(linestr[:min(64, len(linestr))])
|
||||
if offset < len(linestr) {
|
||||
offset = len(trimLastInvalidRune(linestr[:offset]))
|
||||
} else {
|
||||
offset = len(linestr)
|
||||
}
|
||||
column = runewidth.StringWidth(linestr[:offset])
|
||||
return
|
||||
}
|
||||
|
||||
func getLineByLine(str string, line int) (linestr string) {
|
||||
ss := &stringScanner{str, 0}
|
||||
for {
|
||||
str, _, ok := ss.next()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if line--; line == 0 {
|
||||
linestr = str
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(linestr) > 64 {
|
||||
linestr = trimLastInvalidRune(linestr[:64])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func trimLastInvalidRune(s string) string {
|
||||
for i := len(s) - 1; i >= 0 && i > len(s)-utf8.UTFMax; i-- {
|
||||
if b := s[i]; b < utf8.RuneSelf {
|
||||
return s[:i+1]
|
||||
} else if utf8.RuneStart(b) {
|
||||
if r, _ := utf8.DecodeRuneInString(s[i:]); r == utf8.RuneError {
|
||||
return s[:i]
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func formatLineInfo(linestr string, line, column int) string {
|
||||
l := strconv.Itoa(line)
|
||||
return fmt.Sprintf(" %s | %s\n %*c", l, linestr, column+len(l)+4, '^')
|
||||
}
|
||||
|
||||
type stringScanner struct {
|
||||
str string
|
||||
offset int
|
||||
}
|
||||
|
||||
func (ss *stringScanner) next() (line string, start int, ok bool) {
|
||||
if ss.offset == len(ss.str) {
|
||||
return
|
||||
}
|
||||
start, ok = ss.offset, true
|
||||
line = ss.str[start:]
|
||||
i := indexNewline(line)
|
||||
if i < 0 {
|
||||
ss.offset = len(ss.str)
|
||||
return
|
||||
}
|
||||
line = line[:i]
|
||||
if strings.HasPrefix(ss.str[start+i:], "\r\n") {
|
||||
i++
|
||||
}
|
||||
ss.offset += i + 1
|
||||
return
|
||||
}
|
||||
|
||||
// Faster than strings.ContainsAny(str, "\r\n").
|
||||
func containsNewline(str string) bool {
|
||||
return strings.IndexByte(str, '\n') >= 0 ||
|
||||
strings.IndexByte(str, '\r') >= 0
|
||||
}
|
||||
|
||||
// Faster than strings.IndexAny(str, "\r\n").
|
||||
func indexNewline(str string) (i int) {
|
||||
if i = strings.IndexByte(str, '\n'); i >= 0 {
|
||||
str = str[:i]
|
||||
}
|
||||
if j := strings.IndexByte(str, '\r'); j >= 0 {
|
||||
i = j
|
||||
}
|
||||
return
|
||||
}
|
|
@ -1,211 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseFlags(args []string, opts any) ([]string, error) {
|
||||
rest := make([]string, 0, len(args))
|
||||
val := reflect.ValueOf(opts).Elem()
|
||||
typ := val.Type()
|
||||
longToValue := map[string]reflect.Value{}
|
||||
longToPositional := map[string]struct{}{}
|
||||
shortToValue := map[string]reflect.Value{}
|
||||
for i, l := 0, val.NumField(); i < l; i++ {
|
||||
if flag, ok := typ.Field(i).Tag.Lookup("long"); ok {
|
||||
longToValue[flag] = val.Field(i)
|
||||
if _, ok := typ.Field(i).Tag.Lookup("positional"); ok {
|
||||
longToPositional[flag] = struct{}{}
|
||||
}
|
||||
}
|
||||
if flag, ok := typ.Field(i).Tag.Lookup("short"); ok {
|
||||
shortToValue[flag] = val.Field(i)
|
||||
}
|
||||
}
|
||||
mapKeys := map[string]struct{}{}
|
||||
var positionalVal reflect.Value
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
var (
|
||||
val reflect.Value
|
||||
ok bool
|
||||
shortopts string
|
||||
)
|
||||
if arg == "--" {
|
||||
if positionalVal.IsValid() {
|
||||
for _, arg := range args[i+1:] {
|
||||
positionalVal.Set(reflect.Append(positionalVal, reflect.ValueOf(arg)))
|
||||
}
|
||||
} else {
|
||||
rest = append(rest, args[i+1:]...)
|
||||
}
|
||||
break
|
||||
}
|
||||
if strings.HasPrefix(arg, "--") {
|
||||
if val, ok = longToValue[arg[2:]]; !ok {
|
||||
if j := strings.IndexByte(arg, '='); j >= 0 {
|
||||
if val, ok = longToValue[arg[2:j]]; ok {
|
||||
if val.Kind() == reflect.Bool {
|
||||
return nil, fmt.Errorf("boolean flag `%s' cannot have an argument", arg[:j])
|
||||
}
|
||||
args[i] = arg[j+1:]
|
||||
arg = arg[:j]
|
||||
i--
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown flag `%s'", arg)
|
||||
}
|
||||
}
|
||||
} else if len(arg) > 1 && arg[0] == '-' {
|
||||
var skip bool
|
||||
for i := 1; i < len(arg); i++ {
|
||||
opt := arg[i : i+1]
|
||||
if val, ok = shortToValue[opt]; ok {
|
||||
if val.Kind() != reflect.Bool {
|
||||
break
|
||||
}
|
||||
} else if !("A" <= opt && opt <= "Z" || "a" <= opt && opt <= "z") {
|
||||
skip = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !skip && (len(arg) > 2 || !ok) {
|
||||
shortopts = arg[1:]
|
||||
goto L
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
if positionalVal.IsValid() && len(rest) > 0 {
|
||||
positionalVal.Set(reflect.Append(positionalVal, reflect.ValueOf(arg)))
|
||||
} else {
|
||||
rest = append(rest, arg)
|
||||
}
|
||||
continue
|
||||
}
|
||||
S:
|
||||
switch val.Kind() {
|
||||
case reflect.Bool:
|
||||
val.SetBool(true)
|
||||
case reflect.String:
|
||||
if i++; i >= len(args) {
|
||||
return nil, fmt.Errorf("expected argument for flag `%s'", arg)
|
||||
}
|
||||
val.SetString(args[i])
|
||||
case reflect.Ptr:
|
||||
if val.Type().Elem().Kind() == reflect.Int {
|
||||
if i++; i >= len(args) {
|
||||
return nil, fmt.Errorf("expected argument for flag `%s'", arg)
|
||||
}
|
||||
v, err := strconv.Atoi(args[i])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid argument for flag `%s': %w", arg, err)
|
||||
}
|
||||
val.Set(reflect.New(val.Type().Elem()))
|
||||
val.Elem().SetInt(int64(v))
|
||||
}
|
||||
case reflect.Slice:
|
||||
if _, ok := longToPositional[arg[2:]]; ok {
|
||||
if positionalVal.IsValid() {
|
||||
for positionalVal.Len() > val.Len() {
|
||||
val.Set(reflect.Append(val, reflect.Zero(val.Type().Elem())))
|
||||
}
|
||||
}
|
||||
positionalVal = val
|
||||
} else {
|
||||
if i++; i >= len(args) {
|
||||
return nil, fmt.Errorf("expected argument for flag `%s'", arg)
|
||||
}
|
||||
val.Set(reflect.Append(val, reflect.ValueOf(args[i])))
|
||||
}
|
||||
case reflect.Map:
|
||||
if i += 2; i >= len(args) {
|
||||
return nil, fmt.Errorf("expected 2 arguments for flag `%s'", arg)
|
||||
}
|
||||
if val.IsNil() {
|
||||
val.Set(reflect.MakeMap(val.Type()))
|
||||
}
|
||||
name := args[i-1]
|
||||
if _, ok := mapKeys[name]; !ok {
|
||||
mapKeys[name] = struct{}{}
|
||||
val.SetMapIndex(reflect.ValueOf(name), reflect.ValueOf(args[i]))
|
||||
}
|
||||
}
|
||||
L:
|
||||
if shortopts != "" {
|
||||
opt := shortopts[:1]
|
||||
if val, ok = shortToValue[opt]; !ok {
|
||||
return nil, fmt.Errorf("unknown flag `%s'", opt)
|
||||
}
|
||||
if val.Kind() != reflect.Bool && len(shortopts) > 1 {
|
||||
if shortopts[1] == '=' {
|
||||
args[i] = shortopts[2:]
|
||||
} else {
|
||||
args[i] = shortopts[1:]
|
||||
}
|
||||
i--
|
||||
shortopts = ""
|
||||
} else {
|
||||
shortopts = shortopts[1:]
|
||||
}
|
||||
arg = "-" + opt
|
||||
goto S
|
||||
}
|
||||
}
|
||||
return rest, nil
|
||||
}
|
||||
|
||||
func formatFlags(opts any) string {
|
||||
val := reflect.ValueOf(opts).Elem()
|
||||
typ := val.Type()
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Command Options:\n")
|
||||
for i, l := 0, typ.NumField(); i < l; i++ {
|
||||
tag := typ.Field(i).Tag
|
||||
if i == l-1 {
|
||||
sb.WriteString("\nHelp Option:\n")
|
||||
}
|
||||
sb.WriteString(" ")
|
||||
var short bool
|
||||
if flag, ok := tag.Lookup("short"); ok {
|
||||
sb.WriteString("-")
|
||||
sb.WriteString(flag)
|
||||
short = true
|
||||
} else {
|
||||
sb.WriteString(" ")
|
||||
}
|
||||
m := sb.Len()
|
||||
if flag, ok := tag.Lookup("long"); ok {
|
||||
if short {
|
||||
sb.WriteString(", ")
|
||||
} else {
|
||||
sb.WriteString(" ")
|
||||
}
|
||||
sb.WriteString("--")
|
||||
sb.WriteString(flag)
|
||||
switch val.Field(i).Kind() {
|
||||
case reflect.Bool:
|
||||
sb.WriteString(" ")
|
||||
case reflect.Map:
|
||||
if strings.HasSuffix(flag, "file") {
|
||||
sb.WriteString(" name file")
|
||||
} else {
|
||||
sb.WriteString(" name value")
|
||||
}
|
||||
default:
|
||||
if _, ok = tag.Lookup("positional"); !ok {
|
||||
sb.WriteString("=")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sb.WriteString("=")
|
||||
}
|
||||
sb.WriteString(" "[:24-sb.Len()+m])
|
||||
sb.WriteString(tag.Get("description"))
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
module forgejo.owo.monster/chaos/psychonaut_journal_cli/gojq-extended
|
||||
|
||||
go 1.23.2
|
||||
|
||||
require (
|
||||
github.com/itchyny/gojq v0.12.16
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/mattn/go-runewidth v0.0.16
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/itchyny/timefmt-go v0.1.6 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
)
|
|
@ -1,14 +0,0 @@
|
|||
github.com/itchyny/gojq v0.12.16 h1:yLfgLxhIr/6sJNVmYfQjTIv0jGctu6/DgDoivmxTr7g=
|
||||
github.com/itchyny/gojq v0.12.16/go.mod h1:6abHbdC2uB9ogMS38XsErnfqJ94UlngIJGlRAIj4jTM=
|
||||
github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q=
|
||||
github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
@ -1,375 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/itchyny/gojq"
|
||||
)
|
||||
|
||||
type inputReader struct {
|
||||
io.Reader
|
||||
file *os.File
|
||||
buf *bytes.Buffer
|
||||
}
|
||||
|
||||
func newInputReader(r io.Reader) *inputReader {
|
||||
if r, ok := r.(*os.File); ok {
|
||||
if _, err := r.Seek(0, io.SeekCurrent); err == nil {
|
||||
return &inputReader{r, r, nil}
|
||||
}
|
||||
}
|
||||
var buf bytes.Buffer // do not use strings.Builder because we need to Reset
|
||||
return &inputReader{io.TeeReader(r, &buf), nil, &buf}
|
||||
}
|
||||
|
||||
func (ir *inputReader) getContents(offset *int64, line *int) string {
|
||||
if buf := ir.buf; buf != nil {
|
||||
return buf.String()
|
||||
}
|
||||
if current, err := ir.file.Seek(0, io.SeekCurrent); err == nil {
|
||||
defer func() { ir.file.Seek(current, io.SeekStart) }()
|
||||
}
|
||||
ir.file.Seek(0, io.SeekStart)
|
||||
const bufSize = 16 * 1024
|
||||
var buf bytes.Buffer // do not use strings.Builder because we need to Reset
|
||||
if offset != nil && *offset > bufSize {
|
||||
buf.Grow(bufSize)
|
||||
for *offset > bufSize {
|
||||
n, err := io.Copy(&buf, io.LimitReader(ir.file, bufSize))
|
||||
*offset -= int64(n)
|
||||
*line += bytes.Count(buf.Bytes(), []byte{'\n'})
|
||||
buf.Reset()
|
||||
if err != nil || n == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
var r io.Reader
|
||||
if offset == nil {
|
||||
r = ir.file
|
||||
} else {
|
||||
r = io.LimitReader(ir.file, bufSize*2)
|
||||
}
|
||||
io.Copy(&buf, r)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
type inputIter interface {
|
||||
gojq.Iter
|
||||
io.Closer
|
||||
Name() string
|
||||
}
|
||||
|
||||
type jsonInputIter struct {
|
||||
next func() (any, error)
|
||||
ir *inputReader
|
||||
fname string
|
||||
offset int64
|
||||
line int
|
||||
err error
|
||||
}
|
||||
|
||||
func newJSONInputIter(r io.Reader, fname string) inputIter {
|
||||
ir := newInputReader(r)
|
||||
dec := json.NewDecoder(ir)
|
||||
dec.UseNumber()
|
||||
next := func() (v any, err error) { err = dec.Decode(&v); return }
|
||||
return &jsonInputIter{next: next, ir: ir, fname: fname}
|
||||
}
|
||||
|
||||
func (i *jsonInputIter) Next() (any, bool) {
|
||||
if i.err != nil {
|
||||
return nil, false
|
||||
}
|
||||
v, err := i.next()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
i.err = err
|
||||
return nil, false
|
||||
}
|
||||
var offset *int64
|
||||
var line *int
|
||||
if err, ok := err.(*json.SyntaxError); ok {
|
||||
err.Offset -= i.offset
|
||||
offset, line = &err.Offset, &i.line
|
||||
}
|
||||
i.err = &jsonParseError{i.fname, i.ir.getContents(offset, line), i.line, err}
|
||||
return i.err, true
|
||||
}
|
||||
if buf := i.ir.buf; buf != nil && buf.Len() >= 16*1024 {
|
||||
i.offset += int64(buf.Len())
|
||||
i.line += bytes.Count(buf.Bytes(), []byte{'\n'})
|
||||
buf.Reset()
|
||||
}
|
||||
return v, true
|
||||
}
|
||||
|
||||
func (i *jsonInputIter) Close() error {
|
||||
i.err = io.EOF
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *jsonInputIter) Name() string {
|
||||
return i.fname
|
||||
}
|
||||
|
||||
func newStreamInputIter(r io.Reader, fname string) inputIter {
|
||||
ir := newInputReader(r)
|
||||
dec := json.NewDecoder(ir)
|
||||
dec.UseNumber()
|
||||
return &jsonInputIter{next: newJSONStream(dec).next, ir: ir, fname: fname}
|
||||
}
|
||||
|
||||
type nullInputIter struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func newNullInputIter() inputIter {
|
||||
return &nullInputIter{}
|
||||
}
|
||||
|
||||
func (i *nullInputIter) Next() (any, bool) {
|
||||
if i.err != nil {
|
||||
return nil, false
|
||||
}
|
||||
i.err = io.EOF
|
||||
return nil, true
|
||||
}
|
||||
|
||||
func (i *nullInputIter) Close() error {
|
||||
i.err = io.EOF
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*nullInputIter) Name() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type filesInputIter struct {
|
||||
newIter func(io.Reader, string) inputIter
|
||||
fnames []string
|
||||
stdin io.Reader
|
||||
iter inputIter
|
||||
file io.Reader
|
||||
err error
|
||||
}
|
||||
|
||||
func newFilesInputIter(
|
||||
newIter func(io.Reader, string) inputIter, fnames []string, stdin io.Reader,
|
||||
) inputIter {
|
||||
return &filesInputIter{newIter: newIter, fnames: fnames, stdin: stdin}
|
||||
}
|
||||
|
||||
func (i *filesInputIter) Next() (any, bool) {
|
||||
if i.err != nil {
|
||||
return nil, false
|
||||
}
|
||||
for {
|
||||
if i.file == nil {
|
||||
if len(i.fnames) == 0 {
|
||||
i.err = io.EOF
|
||||
if i.iter != nil {
|
||||
i.iter.Close()
|
||||
i.iter = nil
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
fname := i.fnames[0]
|
||||
i.fnames = i.fnames[1:]
|
||||
if fname == "-" && i.stdin != nil {
|
||||
i.file, fname = i.stdin, "<stdin>"
|
||||
} else {
|
||||
file, err := os.Open(fname)
|
||||
if err != nil {
|
||||
return err, true
|
||||
}
|
||||
i.file = file
|
||||
}
|
||||
if i.iter != nil {
|
||||
i.iter.Close()
|
||||
}
|
||||
i.iter = i.newIter(i.file, fname)
|
||||
}
|
||||
if v, ok := i.iter.Next(); ok {
|
||||
return v, ok
|
||||
}
|
||||
if r, ok := i.file.(io.Closer); ok && i.file != i.stdin {
|
||||
r.Close()
|
||||
}
|
||||
i.file = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (i *filesInputIter) Close() error {
|
||||
if i.file != nil {
|
||||
if r, ok := i.file.(io.Closer); ok && i.file != i.stdin {
|
||||
r.Close()
|
||||
}
|
||||
i.file = nil
|
||||
i.err = io.EOF
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *filesInputIter) Name() string {
|
||||
if i.iter != nil {
|
||||
return i.iter.Name()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type rawInputIter struct {
|
||||
r *bufio.Reader
|
||||
fname string
|
||||
err error
|
||||
}
|
||||
|
||||
func newRawInputIter(r io.Reader, fname string) inputIter {
|
||||
return &rawInputIter{r: bufio.NewReader(r), fname: fname}
|
||||
}
|
||||
|
||||
func (i *rawInputIter) Next() (any, bool) {
|
||||
if i.err != nil {
|
||||
return nil, false
|
||||
}
|
||||
line, err := i.r.ReadString('\n')
|
||||
if err != nil {
|
||||
i.err = err
|
||||
if err != io.EOF {
|
||||
return err, true
|
||||
}
|
||||
if line == "" {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
return strings.TrimSuffix(line, "\n"), true
|
||||
}
|
||||
|
||||
func (i *rawInputIter) Close() error {
|
||||
i.err = io.EOF
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *rawInputIter) Name() string {
|
||||
return i.fname
|
||||
}
|
||||
|
||||
type slurpInputIter struct {
|
||||
iter inputIter
|
||||
err error
|
||||
}
|
||||
|
||||
func newSlurpInputIter(iter inputIter) inputIter {
|
||||
return &slurpInputIter{iter: iter}
|
||||
}
|
||||
|
||||
func (i *slurpInputIter) Next() (any, bool) {
|
||||
if i.err != nil {
|
||||
return nil, false
|
||||
}
|
||||
var vs []any
|
||||
var v any
|
||||
var ok bool
|
||||
for {
|
||||
v, ok = i.iter.Next()
|
||||
if !ok {
|
||||
i.err = io.EOF
|
||||
return vs, true
|
||||
}
|
||||
if i.err, ok = v.(error); ok {
|
||||
return i.err, true
|
||||
}
|
||||
vs = append(vs, v)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *slurpInputIter) Close() error {
|
||||
if i.iter != nil {
|
||||
i.iter.Close()
|
||||
i.iter = nil
|
||||
i.err = io.EOF
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *slurpInputIter) Name() string {
|
||||
return i.iter.Name()
|
||||
}
|
||||
|
||||
type readAllIter struct {
|
||||
r io.Reader
|
||||
fname string
|
||||
err error
|
||||
}
|
||||
|
||||
func newReadAllIter(r io.Reader, fname string) inputIter {
|
||||
return &readAllIter{r: r, fname: fname}
|
||||
}
|
||||
|
||||
func (i *readAllIter) Next() (any, bool) {
|
||||
if i.err != nil {
|
||||
return nil, false
|
||||
}
|
||||
i.err = io.EOF
|
||||
cnt, err := io.ReadAll(i.r)
|
||||
if err != nil {
|
||||
return err, true
|
||||
}
|
||||
return string(cnt), true
|
||||
}
|
||||
|
||||
func (i *readAllIter) Close() error {
|
||||
i.err = io.EOF
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *readAllIter) Name() string {
|
||||
return i.fname
|
||||
}
|
||||
|
||||
type slurpRawInputIter struct {
|
||||
iter inputIter
|
||||
err error
|
||||
}
|
||||
|
||||
func newSlurpRawInputIter(iter inputIter) inputIter {
|
||||
return &slurpRawInputIter{iter: iter}
|
||||
}
|
||||
|
||||
func (i *slurpRawInputIter) Next() (any, bool) {
|
||||
if i.err != nil {
|
||||
return nil, false
|
||||
}
|
||||
var vs []string
|
||||
var v any
|
||||
var ok bool
|
||||
for {
|
||||
v, ok = i.iter.Next()
|
||||
if !ok {
|
||||
i.err = io.EOF
|
||||
return strings.Join(vs, ""), true
|
||||
}
|
||||
if i.err, ok = v.(error); ok {
|
||||
return i.err, true
|
||||
}
|
||||
vs = append(vs, v.(string))
|
||||
}
|
||||
}
|
||||
|
||||
func (i *slurpRawInputIter) Close() error {
|
||||
if i.iter != nil {
|
||||
i.iter.Close()
|
||||
i.iter = nil
|
||||
i.err = io.EOF
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *slurpRawInputIter) Name() string {
|
||||
return i.iter.Name()
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type marshaler interface {
|
||||
marshal(any, io.Writer) error
|
||||
}
|
||||
|
||||
type rawMarshaler struct {
|
||||
m marshaler
|
||||
checkNul bool
|
||||
}
|
||||
|
||||
func (m *rawMarshaler) marshal(v any, w io.Writer) error {
|
||||
if s, ok := v.(string); ok {
|
||||
if m.checkNul && strings.ContainsRune(s, '\x00') {
|
||||
return fmt.Errorf("cannot output a string containing NUL character: %q", s)
|
||||
}
|
||||
_, err := w.Write([]byte(s))
|
||||
return err
|
||||
}
|
||||
return m.m.marshal(v, w)
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package main
|
||||
|
||||
import "os"
|
||||
|
||||
// Run gojq.
|
||||
func main() {
|
||||
cli := cli{
|
||||
inStream: os.Stdin,
|
||||
outStream: os.Stdout,
|
||||
errStream: os.Stderr,
|
||||
}
|
||||
os.Exit(cli.run(os.Args[1:]))
|
||||
}
|
|
@ -1,113 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
)
|
||||
|
||||
type jsonStream struct {
|
||||
dec *json.Decoder
|
||||
path []any
|
||||
states []int
|
||||
}
|
||||
|
||||
func newJSONStream(dec *json.Decoder) *jsonStream {
|
||||
return &jsonStream{dec: dec, states: []int{jsonStateTopValue}, path: []any{}}
|
||||
}
|
||||
|
||||
const (
|
||||
jsonStateTopValue = iota
|
||||
jsonStateArrayStart
|
||||
jsonStateArrayValue
|
||||
jsonStateArrayEnd
|
||||
jsonStateArrayEmptyEnd
|
||||
jsonStateObjectStart
|
||||
jsonStateObjectKey
|
||||
jsonStateObjectValue
|
||||
jsonStateObjectEnd
|
||||
jsonStateObjectEmptyEnd
|
||||
)
|
||||
|
||||
func (s *jsonStream) next() (any, error) {
|
||||
switch s.states[len(s.states)-1] {
|
||||
case jsonStateArrayEnd, jsonStateObjectEnd:
|
||||
s.path = s.path[:len(s.path)-1]
|
||||
fallthrough
|
||||
case jsonStateArrayEmptyEnd, jsonStateObjectEmptyEnd:
|
||||
s.states = s.states[:len(s.states)-1]
|
||||
}
|
||||
if s.dec.More() {
|
||||
switch s.states[len(s.states)-1] {
|
||||
case jsonStateArrayValue:
|
||||
s.path[len(s.path)-1] = s.path[len(s.path)-1].(int) + 1
|
||||
case jsonStateObjectValue:
|
||||
s.path = s.path[:len(s.path)-1]
|
||||
}
|
||||
}
|
||||
for {
|
||||
token, err := s.dec.Token()
|
||||
if err != nil {
|
||||
if err == io.EOF && s.states[len(s.states)-1] != jsonStateTopValue {
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if d, ok := token.(json.Delim); ok {
|
||||
switch d {
|
||||
case '[', '{':
|
||||
switch s.states[len(s.states)-1] {
|
||||
case jsonStateArrayStart:
|
||||
s.states[len(s.states)-1] = jsonStateArrayValue
|
||||
case jsonStateObjectKey:
|
||||
s.states[len(s.states)-1] = jsonStateObjectValue
|
||||
}
|
||||
if d == '[' {
|
||||
s.states = append(s.states, jsonStateArrayStart)
|
||||
s.path = append(s.path, 0)
|
||||
} else {
|
||||
s.states = append(s.states, jsonStateObjectStart)
|
||||
}
|
||||
case ']':
|
||||
if s.states[len(s.states)-1] == jsonStateArrayStart {
|
||||
s.states[len(s.states)-1] = jsonStateArrayEmptyEnd
|
||||
s.path = s.path[:len(s.path)-1]
|
||||
return []any{s.copyPath(), []any{}}, nil
|
||||
}
|
||||
s.states[len(s.states)-1] = jsonStateArrayEnd
|
||||
return []any{s.copyPath()}, nil
|
||||
case '}':
|
||||
if s.states[len(s.states)-1] == jsonStateObjectStart {
|
||||
s.states[len(s.states)-1] = jsonStateObjectEmptyEnd
|
||||
return []any{s.copyPath(), map[string]any{}}, nil
|
||||
}
|
||||
s.states[len(s.states)-1] = jsonStateObjectEnd
|
||||
return []any{s.copyPath()}, nil
|
||||
default:
|
||||
panic(d)
|
||||
}
|
||||
} else {
|
||||
switch s.states[len(s.states)-1] {
|
||||
case jsonStateArrayStart:
|
||||
s.states[len(s.states)-1] = jsonStateArrayValue
|
||||
fallthrough
|
||||
case jsonStateArrayValue:
|
||||
return []any{s.copyPath(), token}, nil
|
||||
case jsonStateObjectStart, jsonStateObjectValue:
|
||||
s.states[len(s.states)-1] = jsonStateObjectKey
|
||||
s.path = append(s.path, token)
|
||||
case jsonStateObjectKey:
|
||||
s.states[len(s.states)-1] = jsonStateObjectValue
|
||||
return []any{s.copyPath(), token}, nil
|
||||
default:
|
||||
s.states[len(s.states)-1] = jsonStateTopValue
|
||||
return []any{s.copyPath(), token}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *jsonStream) copyPath() []any {
|
||||
path := make([]any, len(s.path))
|
||||
copy(path, s.path)
|
||||
return path
|
||||
}
|
111
journal/src/helpers.rs
Normal file
111
journal/src/helpers.rs
Normal file
|
@ -0,0 +1,111 @@
|
|||
use crate::types::{CustomUnits, CustomUnitsType, Ingestion};
|
||||
|
||||
pub fn ingestion_dose(ingestion: &Ingestion, custom_units: &CustomUnitsType) -> Option<f64> {
|
||||
if let Some(custom_unit_id) = ingestion.custom_unit_id {
|
||||
if let Some(ingestion_dose) = ingestion.dose {
|
||||
let custom_unit = custom_units
|
||||
.get_by_id(custom_unit_id)
|
||||
.expect("Custom Unit could not be found");
|
||||
|
||||
Some(ingestion_dose * custom_unit.dose)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
ingestion.dose
|
||||
}
|
||||
}
|
||||
|
||||
fn add_standard_deviation(
|
||||
expectation_x: f64,
|
||||
standard_deviation_x: f64,
|
||||
expectation_y: f64,
|
||||
standard_deviation_y: f64,
|
||||
) -> Option<f64> {
|
||||
let sum_x = standard_deviation_x.powi(2) + expectation_x.powi(2);
|
||||
let sum_y = standard_deviation_y.powi(2) + expectation_y.powi(2);
|
||||
|
||||
let expectations = expectation_x.powi(2) + expectation_y.powi(2);
|
||||
|
||||
let product_variance = (sum_x * sum_y) - expectations;
|
||||
|
||||
if product_variance > 0.0000001 {
|
||||
Some((product_variance.sqrt() * 100.0).round() / 100.0)
|
||||
} else {
|
||||
println!(
|
||||
"{} {} {} {}",
|
||||
expectation_x, standard_deviation_x, expectation_y, standard_deviation_y,
|
||||
);
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ingestion_contains_estimate(ingestion: &Ingestion, custom_units: &CustomUnitsType) -> bool {
|
||||
if ingestion.is_estimate {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(custom_unit_id) = ingestion.custom_unit_id {
|
||||
let custom_unit = custom_units
|
||||
.get_by_id(custom_unit_id)
|
||||
.expect("Custom Unit could not be found");
|
||||
|
||||
custom_unit.is_estimate
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ingestion_standard_deviation(
|
||||
ingestion: &Ingestion,
|
||||
custom_units: &CustomUnitsType,
|
||||
) -> Option<f64> {
|
||||
if ingestion.dose.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !ingestion_contains_estimate(ingestion, custom_units) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(custom_unit_id) = ingestion.custom_unit_id {
|
||||
let custom_unit = custom_units
|
||||
.get_by_id(custom_unit_id)
|
||||
.expect("Custom Unit could not be found");
|
||||
|
||||
if custom_unit.estimate_standard_deviation.is_none() {
|
||||
return ingestion.estimate_standard_deviation;
|
||||
};
|
||||
|
||||
if ingestion.estimate_standard_deviation.is_none() {
|
||||
return custom_unit.estimate_standard_deviation;
|
||||
}
|
||||
|
||||
return add_standard_deviation(
|
||||
ingestion.dose.unwrap(),
|
||||
ingestion.estimate_standard_deviation.unwrap(),
|
||||
custom_unit.dose,
|
||||
custom_unit.estimate_standard_deviation.unwrap(),
|
||||
);
|
||||
} else {
|
||||
return ingestion.estimate_standard_deviation;
|
||||
}
|
||||
}
|
||||
|
||||
// def ingestionStandardDeviation($customUnits):
|
||||
// . as $ingestion |
|
||||
// journalTypes::ensureIngestion |
|
||||
|
||||
// if .customUnitId != null then
|
||||
// ($customUnits | map(select(.id == $ingestion.customUnitId))[0]) as $customUnit |
|
||||
|
||||
// ($ingestion.dose // 0) as $expectationX |
|
||||
// ($ingestion.estimatedDoseStandardDeviation // 0) as $standardDeviationX |
|
||||
// ($customUnit.dose // 0) as $expectationY |
|
||||
// ($customUnit.estimatedDoseStandardDeviation // 0) as $standardDeviationY |
|
||||
|
||||
// addStandardDeviations($expectationX; $standardDeviationX; $expectationY; $standardDeviationY)
|
||||
// else
|
||||
// .estimatedDoseStandardDeviation
|
||||
// end;
|
|
@ -1 +1,2 @@
|
|||
pub mod types;
|
||||
pub mod types;
|
||||
pub mod helpers;
|
150
journal/src/types.rs
Normal file
150
journal/src/types.rs
Normal file
|
@ -0,0 +1,150 @@
|
|||
use chrono::serde::ts_milliseconds;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Debug;
|
||||
use std::fmt::Display;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum AdministrationRoute {
|
||||
Oral,
|
||||
Sublingual,
|
||||
Buccal,
|
||||
Insufflated,
|
||||
Rectal,
|
||||
Transdermal,
|
||||
Subcutaneous,
|
||||
Intramuscular,
|
||||
Intravenous,
|
||||
Smoked,
|
||||
Inhaled,
|
||||
}
|
||||
|
||||
impl Display for AdministrationRoute {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Ingestion {
|
||||
pub substance_name: String,
|
||||
#[serde(with = "ts_milliseconds", rename = "time")]
|
||||
pub ingestion_time: DateTime<Utc>,
|
||||
#[serde(with = "ts_milliseconds", rename = "creationDate")]
|
||||
pub creation_time: DateTime<Utc>,
|
||||
pub dose: Option<f64>,
|
||||
#[serde(rename = "isDoseAnEstimate")]
|
||||
pub is_estimate: bool,
|
||||
#[serde(rename = "estimatedDoseStandardDeviation")]
|
||||
pub estimate_standard_deviation: Option<f64>,
|
||||
pub units: String,
|
||||
pub custom_unit_id: Option<i64>,
|
||||
#[serde(rename = "administrationRoute")]
|
||||
pub roa: AdministrationRoute,
|
||||
pub notes: String,
|
||||
pub stomach_fullness: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomSubstance {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub units: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Experience {
|
||||
pub title: String,
|
||||
pub text: String,
|
||||
#[serde(with = "ts_milliseconds", rename = "creationDate")]
|
||||
pub creation_time: DateTime<Utc>,
|
||||
#[serde(with = "ts_milliseconds", rename = "sortDate")]
|
||||
pub modified_time: DateTime<Utc>,
|
||||
pub ingestions: Vec<Ingestion>,
|
||||
}
|
||||
|
||||
pub type ExperiencesType = Vec<Experience>;
|
||||
|
||||
pub trait Experiences {
|
||||
fn filter_by_title(&self, title: String) -> Vec<Experience>;
|
||||
fn get_by_title(&self, title: String) -> Option<Experience>;
|
||||
}
|
||||
|
||||
impl Experiences for ExperiencesType {
|
||||
fn filter_by_title(&self, title: String) -> Vec<Experience> {
|
||||
self.iter()
|
||||
.filter_map(|experience| {
|
||||
if experience.title == title {
|
||||
Some(experience.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
fn get_by_title(&self, title: String) -> Option<Experience> {
|
||||
for experience in self.iter() {
|
||||
if experience.title == title {
|
||||
return Some(experience.clone());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SubstanceCompanion {
|
||||
pub substance_name: String,
|
||||
pub color: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomUnit {
|
||||
pub id: i64,
|
||||
pub substance_name: String,
|
||||
pub name: String,
|
||||
#[serde(with = "ts_milliseconds", rename = "creationDate")]
|
||||
pub creation_time: DateTime<Utc>,
|
||||
pub administration_route: AdministrationRoute,
|
||||
pub dose: f64,
|
||||
pub unit: String,
|
||||
pub original_unit: String,
|
||||
pub is_estimate: bool,
|
||||
#[serde(rename = "estimatedDoseStandardDeviation")]
|
||||
pub estimate_standard_deviation: Option<f64>,
|
||||
pub is_archived: bool,
|
||||
}
|
||||
|
||||
pub type CustomUnitsType = Vec<CustomUnit>;
|
||||
|
||||
pub trait CustomUnits {
|
||||
fn get_by_id(&self, id: i64) -> Option<CustomUnit>;
|
||||
}
|
||||
|
||||
impl CustomUnits for CustomUnitsType {
|
||||
fn get_by_id(&self, id: i64) -> Option<CustomUnit> {
|
||||
for custom_unit in self.iter() {
|
||||
if custom_unit.id == id {
|
||||
return Some(custom_unit.clone());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExportData {
|
||||
pub experiences: ExperiencesType,
|
||||
pub substance_companions: Vec<SubstanceCompanion>,
|
||||
pub custom_substances: Vec<CustomSubstance>,
|
||||
pub custom_units: CustomUnitsType,
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
mod types;
|
||||
|
||||
pub use {
|
||||
types::AdministrationRoute,
|
||||
types::Ingestion,
|
||||
types::CustomSubstance,
|
||||
types::Experience,
|
||||
types::ExportData
|
||||
};
|
|
@ -1,74 +0,0 @@
|
|||
use chrono::serde::ts_milliseconds;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Debug;
|
||||
use std::fmt::Display;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum AdministrationRoute {
|
||||
Oral,
|
||||
Sublingual,
|
||||
Buccal,
|
||||
Insufflated,
|
||||
Rectal,
|
||||
Transdermal,
|
||||
Subcutaneous,
|
||||
Intramuscular,
|
||||
Intravenous,
|
||||
Smoked,
|
||||
Inhaled,
|
||||
}
|
||||
|
||||
impl Display for AdministrationRoute {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Ingestion {
|
||||
pub substance_name: String,
|
||||
#[serde(with = "ts_milliseconds", rename = "time")]
|
||||
pub ingestion_time: DateTime<Utc>,
|
||||
#[serde(with = "ts_milliseconds", rename = "creationDate")]
|
||||
pub creation_time: DateTime<Utc>,
|
||||
pub dose: Option<f64>,
|
||||
#[serde(rename = "isDoseAnEstimate")]
|
||||
pub is_estimate: bool,
|
||||
pub units: String,
|
||||
pub custom_unit_id: Option<i64>,
|
||||
#[serde(rename = "administrationRoute")]
|
||||
pub roa: AdministrationRoute,
|
||||
pub notes: String,
|
||||
pub stomach_fullness: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomSubstance {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub units: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Experience {
|
||||
pub title: String,
|
||||
pub text: String,
|
||||
#[serde(with = "ts_milliseconds", rename = "creationDate")]
|
||||
pub creation_time: DateTime<Utc>,
|
||||
#[serde(with = "ts_milliseconds", rename = "sortDate")]
|
||||
pub modified_time: DateTime<Utc>,
|
||||
pub ingestions: Vec<Ingestion>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExportData {
|
||||
//pub custom_substances: Vec<CustomSubstance>,
|
||||
//pub custom_units: Vec<CustomUnit>,
|
||||
pub experiences: Vec<Experience>,
|
||||
}
|
9
journal_cli/src/args.rs
Normal file
9
journal_cli/src/args.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
use clap::Parser;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap()]
|
||||
pub struct Args {
|
||||
pub export_file: String,
|
||||
// #[clap(subcommand)]
|
||||
// pub command: Commands,
|
||||
}
|
49
journal_cli/src/main.rs
Normal file
49
journal_cli/src/main.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
use journal;
|
||||
use journal::helpers::{ingestion_contains_estimate, ingestion_dose, ingestion_standard_deviation};
|
||||
use journal::types::Experiences;
|
||||
use serde_json;
|
||||
use std::fs::File;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
mod args;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args = args::Args::parse();
|
||||
|
||||
let file = File::open(args.export_file)?;
|
||||
|
||||
let mut export_data: journal::types::ExportData = serde_json::from_reader(file)?;
|
||||
|
||||
export_data
|
||||
.experiences
|
||||
.sort_by(|a, b| a.modified_time.cmp(&b.modified_time));
|
||||
|
||||
for experience in export_data.experiences.iter_mut() {
|
||||
experience
|
||||
.ingestions
|
||||
.sort_by(|a, b| a.ingestion_time.cmp(&b.ingestion_time));
|
||||
}
|
||||
|
||||
let experience = export_data
|
||||
.experiences
|
||||
.get_by_title("20 Apr 2024".to_string())
|
||||
.unwrap();
|
||||
|
||||
println!("{:#?}", experience);
|
||||
|
||||
for ingestion in experience.ingestions.iter() {
|
||||
println!(
|
||||
"{}: {}{}",
|
||||
ingestion.substance_name,
|
||||
format!("{:.2}", ingestion_dose(ingestion, &export_data.custom_units).unwrap()).trim_end_matches(".00"),
|
||||
if ingestion_contains_estimate(ingestion, &export_data.custom_units) {
|
||||
format!("±{:.2}", ingestion_standard_deviation(ingestion, &export_data.custom_units).or(Some(0.0)).unwrap())
|
||||
} else {
|
||||
"".to_string()
|
||||
}.trim_end_matches(".00")
|
||||
)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
17
src/main.rs
17
src/main.rs
|
@ -1,17 +0,0 @@
|
|||
use std::fs::File;
|
||||
use std::env;
|
||||
use journal;
|
||||
use serde_json;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
let file = File::open(args[1].clone())?;
|
||||
let mut export_data: journal::types::ExportData =
|
||||
serde_json::from_reader(file)?;
|
||||
export_data.experiences.sort_by(|a, b| a.modified_time.cmp(&b.modified_time));
|
||||
for experience in export_data.experiences.iter_mut() {
|
||||
experience.ingestions.sort_by(|a,b| a.ingestion_time.cmp(&b.ingestion_time));
|
||||
}
|
||||
println!("Hello, world! {:?}", export_data);
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in a new issue