This commit is contained in:
chaos 2024-11-19 19:37:54 +00:00
parent f103d12a03
commit f81ce78e6f
23 changed files with 325 additions and 1945 deletions

View file

@ -3,6 +3,10 @@ name = "psychonaut_journal_cli"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
[[bin]]
name = "journal-cli"
path = "journal_cli/src/main.rs"
[dependencies] [dependencies]
journal = { path = "./journal" } journal = { path = "./journal" }
chrono = { version = "0.4.38", features = ["serde"] } chrono = { version = "0.4.38", features = ["serde"] }

View file

@ -1 +0,0 @@
gojq-extended

View file

@ -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.

View file

@ -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.

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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()
}

View file

@ -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
)

View file

@ -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=

View file

@ -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()
}

View file

@ -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)
}

View file

@ -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:]))
}

View file

@ -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
View 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;

View file

@ -1 +1,2 @@
pub mod types; pub mod types;
pub mod helpers;

150
journal/src/types.rs Normal file
View 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,
}

View file

@ -1,9 +0,0 @@
mod types;
pub use {
types::AdministrationRoute,
types::Ingestion,
types::CustomSubstance,
types::Experience,
types::ExportData
};

View file

@ -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
View 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
View 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(())
}

View file

@ -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(())
}