feat: add native Factory Droid hook integration
This commit is contained in:
1 parent
2eff1ae119
commit
577e3609b5
13 files changed
+2292
-1
No files matched your search
@@ -0,0 +1,259 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/tailscale/hujson"
|
||||
)
|
||||
|
||||
type droidJSONCEditor struct {
|
||||
value hujson.Value
|
||||
eol string
|
||||
indent string
|
||||
}
|
||||
|
||||
func standardizeDroidJSONC(source []byte) ([]byte, error) {
|
||||
value, err := parseDroidJSONC(source, "JSONC source")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
value.Standardize()
|
||||
return value.Pack(), nil
|
||||
}
|
||||
|
||||
func parseDroidJSONC(source []byte, label string) (hujson.Value, error) {
|
||||
value, err := hujson.Parse(source)
|
||||
if err != nil {
|
||||
return hujson.Value{}, fmt.Errorf("parse %s: %w", label, err)
|
||||
}
|
||||
if err := rejectDroidDuplicateKeys(&value, label, "$"); err != nil {
|
||||
return hujson.Value{}, err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func rejectDroidDuplicateKeys(value *hujson.Value, label, location string) error {
|
||||
switch current := value.Value.(type) {
|
||||
case *hujson.Object:
|
||||
seen := make(map[string]bool, len(current.Members))
|
||||
for index := range current.Members {
|
||||
member := ¤t.Members[index]
|
||||
name := member.Name.Value.(hujson.Literal).String()
|
||||
if seen[name] {
|
||||
return fmt.Errorf("%s contains duplicate key %q at %s", label, name, location)
|
||||
}
|
||||
seen[name] = true
|
||||
if err := rejectDroidDuplicateKeys(&member.Value, label, location+"/"+name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case *hujson.Array:
|
||||
for index := range current.Elements {
|
||||
if err := rejectDroidDuplicateKeys(
|
||||
¤t.Elements[index], label, location+"/"+strconv.Itoa(index),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeDroidJSONCObject(source []byte, label string) (map[string]any, error) {
|
||||
value, err := parseDroidJSONC(source, label)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := value.Value.(*hujson.Object); !ok {
|
||||
return nil, fmt.Errorf("%s must contain a JSON object", label)
|
||||
}
|
||||
standard := value.Clone()
|
||||
standard.Standardize()
|
||||
var object map[string]any
|
||||
if err := json.Unmarshal(standard.Pack(), &object); err != nil {
|
||||
return nil, fmt.Errorf("decode %s: %w", label, err)
|
||||
}
|
||||
return object, nil
|
||||
}
|
||||
|
||||
func newDroidJSONCEditor(source []byte, label string) (*droidJSONCEditor, error) {
|
||||
value, err := parseDroidJSONC(source, label)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &droidJSONCEditor{
|
||||
value: value, eol: detectDroidEOL(source), indent: detectDroidIndent(source),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func detectDroidEOL(source []byte) string {
|
||||
if bytes.Contains(source, []byte("\r\n")) {
|
||||
return "\r\n"
|
||||
}
|
||||
return "\n"
|
||||
}
|
||||
|
||||
func detectDroidIndent(source []byte) string {
|
||||
normalized := strings.ReplaceAll(string(source), "\r\n", "\n")
|
||||
for _, line := range strings.Split(normalized, "\n") {
|
||||
trimmed := strings.TrimLeft(line, "\t ")
|
||||
if !strings.HasPrefix(trimmed, `"`) || trimmed == line {
|
||||
continue
|
||||
}
|
||||
return line[:len(line)-len(trimmed)]
|
||||
}
|
||||
return " "
|
||||
}
|
||||
|
||||
func (editor *droidJSONCEditor) pack() []byte {
|
||||
return editor.value.Pack()
|
||||
}
|
||||
|
||||
func (editor *droidJSONCEditor) remove(path []any) error {
|
||||
operation := []map[string]any{{"op": "remove", "path": droidJSONPointer(path)}}
|
||||
patch, err := json.Marshal(operation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode Factory Droid JSONC removal: %w", err)
|
||||
}
|
||||
if err := editor.value.Patch(patch); err != nil {
|
||||
return fmt.Errorf("apply Factory Droid JSONC removal at %s: %w", droidJSONPointer(path), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (editor *droidJSONCEditor) addProperty(objectPath []any, name string, value any) error {
|
||||
container, err := findDroidJSONCValue(&editor.value, objectPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
object, ok := container.Value.(*hujson.Object)
|
||||
if !ok {
|
||||
return fmt.Errorf("Factory Droid JSONC path %s must be an object", droidJSONPointer(objectPath))
|
||||
}
|
||||
for _, member := range object.Members {
|
||||
if member.Name.Value.(hujson.Literal).String() == name {
|
||||
return fmt.Errorf("Factory Droid JSONC property %q already exists", name)
|
||||
}
|
||||
}
|
||||
propertyDepth := len(objectPath) + 1
|
||||
formatted, err := editor.formattedValue(value, propertyDepth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
trailingComma := len(object.Members) > 0 &&
|
||||
object.Members[len(object.Members)-1].Value.AfterExtra != nil
|
||||
leading := editor.newLeadingExtra(object.AfterExtra, propertyDepth)
|
||||
object.AfterExtra = []byte(editor.eol + strings.Repeat(editor.indent, len(objectPath)))
|
||||
member := hujson.ObjectMember{
|
||||
Name: hujson.Value{BeforeExtra: leading, Value: hujson.String(name)},
|
||||
Value: hujson.Value{BeforeExtra: []byte(" "), Value: formatted.Value},
|
||||
}
|
||||
if trailingComma {
|
||||
member.Value.AfterExtra = []byte{}
|
||||
}
|
||||
object.Members = append(object.Members, member)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (editor *droidJSONCEditor) appendArray(arrayPath []any, value any) error {
|
||||
container, err := findDroidJSONCValue(&editor.value, arrayPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
array, ok := container.Value.(*hujson.Array)
|
||||
if !ok {
|
||||
return fmt.Errorf("Factory Droid JSONC path %s must be an array", droidJSONPointer(arrayPath))
|
||||
}
|
||||
arrayDepth := len(arrayPath)
|
||||
formatted, err := editor.formattedValue(value, arrayDepth+1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
trailingComma := len(array.Elements) > 0 && array.Elements[len(array.Elements)-1].AfterExtra != nil
|
||||
formatted.BeforeExtra = editor.newLeadingExtra(array.AfterExtra, arrayDepth+1)
|
||||
if trailingComma {
|
||||
formatted.AfterExtra = []byte{}
|
||||
} else {
|
||||
formatted.AfterExtra = nil
|
||||
}
|
||||
array.AfterExtra = []byte(editor.eol + strings.Repeat(editor.indent, arrayDepth))
|
||||
array.Elements = append(array.Elements, formatted)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (editor *droidJSONCEditor) formattedValue(value any, depth int) (hujson.Value, error) {
|
||||
prefix := strings.Repeat(editor.indent, depth)
|
||||
encoded, err := json.MarshalIndent(value, prefix, editor.indent)
|
||||
if err != nil {
|
||||
return hujson.Value{}, fmt.Errorf("encode Factory Droid hook value: %w", err)
|
||||
}
|
||||
if editor.eol != "\n" {
|
||||
encoded = bytes.ReplaceAll(encoded, []byte("\n"), []byte(editor.eol))
|
||||
}
|
||||
parsed, err := hujson.Parse(encoded)
|
||||
if err != nil {
|
||||
return hujson.Value{}, fmt.Errorf("parse generated Factory Droid hook value: %w", err)
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func (editor *droidJSONCEditor) newLeadingExtra(current hujson.Extra, depth int) hujson.Extra {
|
||||
standard := []byte(editor.eol + strings.Repeat(editor.indent, depth))
|
||||
if bytes.Contains(current, []byte("//")) || bytes.Contains(current, []byte("/*")) {
|
||||
combined := append(hujson.Extra(nil), current...)
|
||||
return append(combined, standard...)
|
||||
}
|
||||
return standard
|
||||
}
|
||||
|
||||
func findDroidJSONCValue(root *hujson.Value, path []any) (*hujson.Value, error) {
|
||||
current := root
|
||||
for _, part := range path {
|
||||
switch key := part.(type) {
|
||||
case string:
|
||||
object, ok := current.Value.(*hujson.Object)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Factory Droid JSONC path %s must traverse an object", droidJSONPointer(path))
|
||||
}
|
||||
found := false
|
||||
for index := range object.Members {
|
||||
if object.Members[index].Name.Value.(hujson.Literal).String() == key {
|
||||
current = &object.Members[index].Value
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("Factory Droid JSONC path %s does not exist", droidJSONPointer(path))
|
||||
}
|
||||
case int:
|
||||
array, ok := current.Value.(*hujson.Array)
|
||||
if !ok || key < 0 || key >= len(array.Elements) {
|
||||
return nil, fmt.Errorf("Factory Droid JSONC array path %s is out of range", droidJSONPointer(path))
|
||||
}
|
||||
current = &array.Elements[key]
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported Factory Droid JSONC path part %T", part)
|
||||
}
|
||||
}
|
||||
return current, nil
|
||||
}
|
||||
|
||||
func droidJSONPointer(path []any) string {
|
||||
var pointer strings.Builder
|
||||
for _, part := range path {
|
||||
pointer.WriteByte('/')
|
||||
switch value := part.(type) {
|
||||
case string:
|
||||
value = strings.ReplaceAll(value, "~", "~0")
|
||||
pointer.WriteString(strings.ReplaceAll(value, "/", "~1"))
|
||||
case int:
|
||||
pointer.WriteString(strconv.Itoa(value))
|
||||
}
|
||||
}
|
||||
return pointer.String()
|
||||
}
|
||||
Reference in new issue
Block a user