waveterm/pkg/util/utilfn/partial.go
Mike Sawka 7ac2f25b04
big improvements to waveapp builder (#2550)
* build manifest
* working on secrets injection (secretstore + secret-bindings.json)
* tool progress indicators
* build output and errors injected as the result of the edit calls so AI
gets instant feedback on edits
* change edits to not be atomic (allows AI to make better progress)
* updated binary location for waveapps
* publish button
* new partial json parser (for sending incremental tool progress
indication)
* updated tsunami view to use new embedded scaffold + config vars
* lots of work on cleaning up the output so it is more useful to users +
AI agents
* fix builder init flow
2025-11-13 17:54:17 -08:00

175 lines
3 KiB
Go

// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package utilfn
import (
"encoding/json"
)
type stackItem int
const (
stackInvalid stackItem = iota
stackLBrace
stackLBrack
stackBeforeKey
stackKey
stackKeyColon
stackQuote
)
type jsonStack []stackItem
func (s *jsonStack) push(item stackItem) {
*s = append(*s, item)
}
func (s *jsonStack) pop() stackItem {
if len(*s) == 0 {
return stackInvalid
}
item := (*s)[len(*s)-1]
*s = (*s)[:len(*s)-1]
return item
}
func (s jsonStack) peek() stackItem {
if len(s) == 0 {
return stackInvalid
}
return s[len(s)-1]
}
func (s jsonStack) isTop(items ...stackItem) bool {
top := s.peek()
for _, item := range items {
if top == item {
return true
}
}
return false
}
func (s *jsonStack) replaceTop(item stackItem) {
if len(*s) > 0 {
(*s)[len(*s)-1] = item
}
}
func repairJson(data []byte) []byte {
if len(data) == 0 {
return data
}
var stack jsonStack
inString := false
escaped := false
lastComma := false
for i := 0; i < len(data); i++ {
b := data[i]
if escaped {
escaped = false
continue
}
if inString {
if b == '\\' {
escaped = true
continue
}
if b == '"' {
inString = false
}
continue
}
if b == ' ' || b == '\t' || b == '\n' || b == '\r' {
continue
}
valueStart := b == '{' || b == '[' || b == 'n' || b == 't' || b == 'f' || b == '"' || (b >= '0' && b <= '9') || b == '-'
if valueStart && lastComma {
lastComma = false
}
if valueStart && stack.isTop(stackKeyColon) {
stack.pop()
}
if valueStart && stack.isTop(stackBeforeKey) {
stack.replaceTop(stackKey)
}
switch b {
case '{':
stack.push(stackLBrace)
stack.push(stackBeforeKey)
case '[':
stack.push(stackLBrack)
case '}':
if stack.isTop(stackBeforeKey) {
stack.pop()
}
if stack.isTop(stackLBrace) {
stack.pop()
}
case ']':
if stack.isTop(stackLBrack) {
stack.pop()
}
case '"':
inString = true
case ':':
if stack.isTop(stackKey) {
stack.replaceTop(stackKeyColon)
}
case ',':
lastComma = true
if stack.isTop(stackLBrace) {
stack.push(stackBeforeKey)
}
default:
}
}
if len(stack) == 0 && !inString {
return data
}
result := append([]byte{}, data...)
if escaped && len(result) > 0 {
result = result[:len(result)-1]
}
if inString {
result = append(result, '"')
}
if lastComma {
for i := len(result) - 1; i >= 0; i-- {
if result[i] == ',' {
result = result[:i]
break
}
}
}
for i := len(stack) - 1; i >= 0; i-- {
switch stack[i] {
case stackKeyColon:
result = append(result, []byte("null")...)
case stackKey:
result = append(result, []byte(": null")...)
case stackLBrace:
result = append(result, '}')
case stackLBrack:
result = append(result, ']')
}
}
return result
}
func ParsePartialJson(data []byte) (any, error) {
fixedData := repairJson(data)
var output any
err := json.Unmarshal(fixedData, &output)
if err != nil {
return nil, err
}
return output, nil
}