1
0
Fork 0
timelinize/tlzapp/cli2api.go
Matthew Holt c9db392d20
Implement timeline settings stored in DB; toggle semantic features
I don't love that the type has to be stored in the table... it would be great if we could infer it, but I don't know how that would work for strings that look like another type.
2025-09-05 16:27:17 -06:00

164 lines
4.8 KiB
Go

/*
Timelinize
Copyright (c) 2013 Matthew Holt
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package tlzapp
import (
"encoding/json"
"fmt"
"net/url"
"strconv"
"strings"
)
// makeForm parses args and encodes the data as urlencoded-data.
func makeForm(args []string) string {
formVals := url.Values{}
keyVals := flagValPairs(args)
for _, pair := range keyVals {
formVals.Add(sanitizeFlag(pair.flag), fmt.Sprintf("%v", pair.val))
}
return formVals.Encode()
}
// sanitizeFlag turns a flag string like "--foo-bar"
// into "foo_bar"; i.e. it strips the flag prefix
// and standardizes its format.
func sanitizeFlag(s string) string {
// figure out how long the dash-prefix iss
name := s
switch {
case strings.HasPrefix(s, "--"):
name = s[2:]
case strings.HasPrefix(s, "-"):
name = s[1:]
}
// Command line convention is to use -, but our JSON keys typically use _.
// TODO: This condition is kind of a hack. Timeline UUIDs have "-" and replacing them with
// "_" will obviously break things... if flag name is part of an object, we assume user
// has done the conversion already and we will just take it literally. This is not the
// correct fix though. I'm not sure what is (that won't interfere with shell syntax).
if !strings.Contains(name, ".") {
name = strings.ReplaceAll(name, "-", "_")
}
return name
}
// makeJSON parses args and encodes the data as JSON.
func makeJSON(args []string) ([]byte, error) {
if len(args) == 0 {
return nil, nil
}
// special cases: if there is only one arg and it is not
// a flag, it must be a simple JSON data type.
if len(args) == 1 && !isFlag(args[0]) {
return json.Marshal(autoType(args[0]))
}
// create the object we will populate based on flags
var obj any
// match up each flag to its value
keyVals := flagValPairs(args)
for _, pair := range keyVals {
// strip flag prefix and format flag as a JSON object key
cleanFlag := sanitizeFlag(pair.flag)
// split the key into parts so we can traverse the object;
// object nesting is separated with dots
flagParts := strings.Split(cleanFlag, ".")
// for mixed parts that have both a key and an array index (such as
// "foo[0]") split them as if it was "foo.[0]" -- this is just syntactic
// sugar, because the former feels more natural and familiar
for j := 0; j < len(flagParts); j++ {
part := flagParts[j]
if k := strings.Index(part, "["); k > 0 && part[len(part)-1] == ']' {
keyPart := part[:k]
arrPart := part[k:]
newParts := []string{keyPart, arrPart}
flagParts = append(flagParts[:j], append(newParts, flagParts[j+1:]...)...)
}
}
// now traverse the flag, part by part, recursively,
// constructing the object as we go
var err error
obj, err = traverse(obj, flagParts, pair.val)
if err != nil {
return nil, err
}
}
return json.Marshal(obj)
}
// traverse recursively builds obj from the bottom-up. On the way down, it
// turns nil obj into either an array or a map, and on the way back up,
// it assigns values to the new structure according to the first flagPart.
func traverse(obj any, flagParts []string, val any) (any, error) {
if len(flagParts) == 0 {
return val, nil
}
// part will either be an array index ("[0]") or an object key ("foo")
part := flagParts[0]
if len(part) > 1 && part[0] == '[' && part[len(part)-1] == ']' {
// part is an array index
idx, err := strconv.Atoi(part[1 : len(part)-1])
if err != nil {
return obj, fmt.Errorf("invalid array index %s", part)
}
if obj == nil {
obj = make([]any, idx+1)
}
if objArr, ok := obj.([]any); ok {
if len(objArr) <= idx {
objArr = append(objArr, make([]any, idx-len(objArr)+1)...)
}
objArr[idx], err = traverse(objArr[idx], flagParts[1:], val)
if err != nil {
return obj, err
}
return objArr, nil
}
return obj, fmt.Errorf("inconsistent structure: expected an array at %s but got %T", part, obj)
}
// part is an object key
if obj == nil {
obj = make(map[string]any)
}
if objMap, ok := obj.(map[string]any); ok {
var err error
objMap[part], err = traverse(objMap[part], flagParts[1:], val)
if err != nil {
return obj, err
}
return objMap, nil
}
return obj, fmt.Errorf("inconsistent structure: expected a map at %s but got %T", part, obj)
}