Skip to content

Commit 8c47b82

Browse files
authored
implement /api/schemas (json schemas from tsunami atoms /api/config /api/data) (#2335)
1 parent 2783eeb commit 8c47b82

File tree

15 files changed

+661
-115
lines changed

15 files changed

+661
-115
lines changed

tsunami/app/atom.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,22 @@ import (
1212
"github.com/wavetermdev/waveterm/tsunami/util"
1313
)
1414

15+
// AtomMeta provides metadata about an atom for validation and documentation
16+
type AtomMeta struct {
17+
Desc string // short, user-facing
18+
Units string // "ms", "GiB", etc.
19+
Min *float64 // optional minimum (numeric types)
20+
Max *float64 // optional maximum (numeric types)
21+
Enum []string // allowed values if finite set
22+
Pattern string // regex constraint for strings
23+
}
24+
25+
// Atom[T] represents a typed atom implementation
26+
type Atom[T any] struct {
27+
name string
28+
client *engine.ClientImpl
29+
}
30+
1531
// logInvalidAtomSet logs an error when an atom is being set during component render
1632
func logInvalidAtomSet(atomName string) {
1733
_, file, line, ok := runtime.Caller(2)
@@ -58,12 +74,6 @@ func logMutationWarning(atomName string) {
5874
}
5975
}
6076

61-
// Atom[T] represents a typed atom implementation
62-
type Atom[T any] struct {
63-
name string
64-
client *engine.ClientImpl
65-
}
66-
6777
// AtomName implements the vdom.Atom interface
6878
func (a Atom[T]) AtomName() string {
6979
return a.name

tsunami/app/defaultclient.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ func DefineComponent[P any](name string, renderFn func(props P) any) vdom.Compon
1616
return engine.DefineComponentEx(engine.GetDefaultClient(), name, renderFn)
1717
}
1818

19+
func Ptr[T any](v T) *T {
20+
return &v
21+
}
22+
1923
func SetGlobalEventHandler(handler func(event vdom.VDomEvent)) {
2024
engine.GetDefaultClient().SetGlobalEventHandler(handler)
2125
}
@@ -35,30 +39,46 @@ func SendAsyncInitiation() error {
3539
return engine.GetDefaultClient().SendAsyncInitiation()
3640
}
3741

38-
func ConfigAtom[T any](name string, defaultValue T) Atom[T] {
42+
func ConfigAtom[T any](name string, defaultValue T, meta *AtomMeta) Atom[T] {
3943
fullName := "$config." + name
4044
client := engine.GetDefaultClient()
41-
atom := engine.MakeAtomImpl(defaultValue)
45+
engineMeta := convertAppMetaToEngineMeta(meta)
46+
atom := engine.MakeAtomImpl(defaultValue, engineMeta)
4247
client.Root.RegisterAtom(fullName, atom)
4348
return Atom[T]{name: fullName, client: client}
4449
}
4550

46-
func DataAtom[T any](name string, defaultValue T) Atom[T] {
51+
func DataAtom[T any](name string, defaultValue T, meta *AtomMeta) Atom[T] {
4752
fullName := "$data." + name
4853
client := engine.GetDefaultClient()
49-
atom := engine.MakeAtomImpl(defaultValue)
54+
engineMeta := convertAppMetaToEngineMeta(meta)
55+
atom := engine.MakeAtomImpl(defaultValue, engineMeta)
5056
client.Root.RegisterAtom(fullName, atom)
5157
return Atom[T]{name: fullName, client: client}
5258
}
5359

5460
func SharedAtom[T any](name string, defaultValue T) Atom[T] {
5561
fullName := "$shared." + name
5662
client := engine.GetDefaultClient()
57-
atom := engine.MakeAtomImpl(defaultValue)
63+
atom := engine.MakeAtomImpl(defaultValue, nil)
5864
client.Root.RegisterAtom(fullName, atom)
5965
return Atom[T]{name: fullName, client: client}
6066
}
6167

68+
func convertAppMetaToEngineMeta(appMeta *AtomMeta) *engine.AtomMeta {
69+
if appMeta == nil {
70+
return nil
71+
}
72+
return &engine.AtomMeta{
73+
Description: appMeta.Desc,
74+
Units: appMeta.Units,
75+
Min: appMeta.Min,
76+
Max: appMeta.Max,
77+
Enum: appMeta.Enum,
78+
Pattern: appMeta.Pattern,
79+
}
80+
}
81+
6282
// HandleDynFunc registers a dynamic HTTP handler function with the internal http.ServeMux.
6383
// The pattern MUST start with "/dyn/" to be valid. This allows registration of dynamic
6484
// routes that can be handled at runtime.

tsunami/demo/cpuchart/app.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ import (
1111

1212
// Global atoms for config and data
1313
var (
14-
dataPointCountAtom = app.ConfigAtom("dataPointCount", 60)
15-
cpuDataAtom = app.DataAtom("cpuData", func() []CPUDataPoint {
14+
dataPointCountAtom = app.ConfigAtom("dataPointCount", 60, &app.AtomMeta{
15+
Desc: "Number of CPU data points to display in the chart",
16+
Min: app.Ptr(10.0),
17+
Max: app.Ptr(300.0),
18+
})
19+
cpuDataAtom = app.DataAtom("cpuData", func() []CPUDataPoint {
1620
// Initialize with empty data points to maintain consistent chart size
1721
dataPointCount := 60 // Default value for initialization
1822
initialData := make([]CPUDataPoint, dataPointCount)
@@ -24,13 +28,15 @@ var (
2428
}
2529
}
2630
return initialData
27-
}())
31+
}(), &app.AtomMeta{
32+
Desc: "Historical CPU usage data points for charting",
33+
})
2834
)
2935

3036
type CPUDataPoint struct {
31-
Time int64 `json:"time"` // Unix timestamp in seconds
32-
CPUUsage *float64 `json:"cpuUsage"` // CPU usage percentage (nil for empty slots)
33-
Timestamp string `json:"timestamp"` // Human readable timestamp
37+
Time int64 `json:"time" desc:"Unix timestamp (seconds since epoch)" units:"s"`
38+
CPUUsage *float64 `json:"cpuUsage" desc:"CPU usage percentage" units:"%" min:"0" max:"100"`
39+
Timestamp string `json:"timestamp" desc:"Human-readable HH:MM:SS"`
3440
}
3541

3642
type StatsPanelProps struct {
@@ -207,7 +213,7 @@ var App = app.DefineComponent("App", func(_ struct{}) any {
207213
}, "Real-Time CPU Usage Monitor"),
208214
vdom.H("p", map[string]any{
209215
"className": "text-gray-400",
210-
}, "Live CPU usage data collected using gopsutil, displaying 60 seconds of history"),
216+
}, "Live CPU usage data collected using gopsutil, displaying ", dataPointCount, " seconds of history"),
211217
),
212218

213219
// Controls
@@ -334,7 +340,7 @@ var App = app.DefineComponent("App", func(_ struct{}) any {
334340
vdom.H("span", map[string]any{
335341
"className": "text-blue-400 mt-1",
336342
}, "•"),
337-
"Rolling window of 60 seconds of historical data",
343+
"Rolling window of ", dataPointCount, " seconds of historical data",
338344
),
339345
vdom.H("li", map[string]any{
340346
"className": "flex items-start gap-2",

tsunami/demo/githubaction/app.go

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,37 @@ import (
1717

1818
// Global atoms for config and data
1919
var (
20-
pollIntervalAtom = app.ConfigAtom("pollInterval", 5)
21-
repositoryAtom = app.ConfigAtom("repository", "wavetermdev/waveterm")
22-
workflowAtom = app.ConfigAtom("workflow", "build-helper.yml")
23-
maxWorkflowRunsAtom = app.ConfigAtom("maxWorkflowRuns", 10)
24-
workflowRunsAtom = app.DataAtom("workflowRuns", []WorkflowRun{})
25-
lastErrorAtom = app.DataAtom("lastError", "")
26-
isLoadingAtom = app.DataAtom("isLoading", true)
27-
lastRefreshTimeAtom = app.DataAtom("lastRefreshTime", time.Time{})
20+
pollIntervalAtom = app.ConfigAtom("pollInterval", 5, &app.AtomMeta{
21+
Desc: "Polling interval for GitHub API requests",
22+
Units: "s",
23+
Min: app.Ptr(1.0),
24+
Max: app.Ptr(300.0),
25+
})
26+
repositoryAtom = app.ConfigAtom("repository", "wavetermdev/waveterm", &app.AtomMeta{
27+
Desc: "GitHub repository in owner/repo format",
28+
Pattern: `^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$`,
29+
})
30+
workflowAtom = app.ConfigAtom("workflow", "build-helper.yml", &app.AtomMeta{
31+
Desc: "GitHub Actions workflow file name",
32+
Pattern: `^.+\.(yml|yaml)$`,
33+
})
34+
maxWorkflowRunsAtom = app.ConfigAtom("maxWorkflowRuns", 10, &app.AtomMeta{
35+
Desc: "Maximum number of workflow runs to fetch",
36+
Min: app.Ptr(1.0),
37+
Max: app.Ptr(100.0),
38+
})
39+
workflowRunsAtom = app.DataAtom("workflowRuns", []WorkflowRun{}, &app.AtomMeta{
40+
Desc: "List of GitHub Actions workflow runs",
41+
})
42+
lastErrorAtom = app.DataAtom("lastError", "", &app.AtomMeta{
43+
Desc: "Last error message from GitHub API",
44+
})
45+
isLoadingAtom = app.DataAtom("isLoading", true, &app.AtomMeta{
46+
Desc: "Loading state for workflow data fetch",
47+
})
48+
lastRefreshTimeAtom = app.DataAtom("lastRefreshTime", time.Time{}, &app.AtomMeta{
49+
Desc: "Timestamp of last successful data refresh",
50+
})
2851
)
2952

3053
type WorkflowRun struct {
@@ -388,7 +411,7 @@ var App = app.DefineComponent("App",
388411
vdom.H("span", map[string]any{
389412
"className": "text-blue-400 mt-1",
390413
}, "•"),
391-
"Polls GitHub API every 5 seconds for real-time updates",
414+
"Polls GitHub API every ", pollInterval, " seconds for real-time updates",
392415
),
393416
vdom.H("li", map[string]any{
394417
"className": "flex items-start gap-2",

tsunami/demo/pomodoro/app.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ var (
1818
BreakMode = Mode{Name: "Break", Duration: 5}
1919

2020
// Data atom to expose remaining seconds to external systems
21-
remainingSecondsAtom = app.DataAtom("remainingSeconds", WorkMode.Duration*60)
21+
remainingSecondsAtom = app.DataAtom("remainingSeconds", WorkMode.Duration*60, &app.AtomMeta{
22+
Desc: "Remaining seconds in current pomodoro timer",
23+
Units: "s",
24+
Min: app.Ptr(0.0),
25+
Max: app.Ptr(3600.0),
26+
})
2227
)
2328

2429
type TimerDisplayProps struct {
@@ -34,7 +39,6 @@ type ControlButtonsProps struct {
3439
OnMode func(int) `json:"onMode"`
3540
}
3641

37-
3842
var TimerDisplay = app.DefineComponent("TimerDisplay",
3943
func(props TimerDisplayProps) any {
4044
minutes := props.RemainingSeconds / 60
@@ -151,7 +155,7 @@ var App = app.DefineComponent("App",
151155
if !isRunning.Get() {
152156
return
153157
}
154-
158+
155159
// Calculate remaining time and update remainingSeconds
156160
elapsed := time.Since(startTime.Current)
157161
remaining := totalDuration.Current - elapsed
@@ -190,7 +194,7 @@ var App = app.DefineComponent("App",
190194
),
191195
TimerDisplay(TimerDisplayProps{
192196
RemainingSeconds: remainingSecondsAtom.Get(),
193-
Mode: mode.Get(),
197+
Mode: mode.Get(),
194198
}),
195199
ControlButtons(ControlButtonsProps{
196200
IsRunning: isRunning.Get(),

tsunami/demo/recharts/app.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,16 @@ import (
1010

1111
// Global atoms for config and data
1212
var (
13-
chartDataAtom = app.DataAtom("chartData", generateInitialData())
14-
chartTypeAtom = app.ConfigAtom("chartType", "line")
15-
isAnimatingAtom = app.SharedAtom("isAnimating", false)
13+
chartDataAtom = app.DataAtom("chartData", generateInitialData(), &app.AtomMeta{
14+
Desc: "Chart data points for system metrics visualization",
15+
})
16+
chartTypeAtom = app.ConfigAtom("chartType", "line", &app.AtomMeta{
17+
Desc: "Type of chart to display",
18+
Enum: []string{"line", "area", "bar"},
19+
})
20+
isAnimatingAtom = app.ConfigAtom("isAnimating", false, &app.AtomMeta{
21+
Desc: "Whether the chart is currently animating with live data",
22+
})
1623
)
1724

1825
type DataPoint struct {

tsunami/demo/tabletest/app.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ var sampleData = app.DataAtom("sampleData", []Person{
3131
{Name: "Henry Taylor", Age: 33, Email: "[email protected]", City: "San Diego"},
3232
{Name: "Ivy Chen", Age: 26, Email: "[email protected]", City: "Dallas"},
3333
{Name: "Jack Anderson", Age: 31, Email: "[email protected]", City: "San Jose"},
34+
}, &app.AtomMeta{
35+
Desc: "Sample person data for table display testing",
3436
})
3537

3638
// The App component is the required entry point for every Tsunami application

tsunami/demo/tsunamiconfig/app.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ import (
1616

1717
// Global atoms for config
1818
var (
19-
serverURLAtom = app.ConfigAtom("serverURL", "")
19+
serverURLAtom = app.ConfigAtom("serverURL", "", &app.AtomMeta{
20+
Desc: "Server URL for config API (can be full URL, hostname:port, or just port)",
21+
Pattern: `^(https?://.*|[a-zA-Z0-9.-]+:\d+|\d+|[a-zA-Z0-9.-]+)$`,
22+
})
2023
)
2124

2225
type URLInputProps struct {

tsunami/engine/atomimpl.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,33 @@ package engine
66
import (
77
"encoding/json"
88
"fmt"
9+
"reflect"
910
"sync"
1011
)
1112

13+
// AtomMeta provides metadata about an atom for validation and documentation
14+
type AtomMeta struct {
15+
Description string // short, user-facing
16+
Units string // "ms", "GiB", etc.
17+
Min *float64 // optional minimum (numeric types)
18+
Max *float64 // optional maximum (numeric types)
19+
Enum []string // allowed values if finite set
20+
Pattern string // regex constraint for strings
21+
}
22+
1223
type AtomImpl[T any] struct {
1324
lock *sync.Mutex
1425
val T
1526
usedBy map[string]bool // component waveid -> true
27+
meta *AtomMeta // optional metadata
1628
}
1729

18-
func MakeAtomImpl[T any](initialVal T) *AtomImpl[T] {
30+
func MakeAtomImpl[T any](initialVal T, meta *AtomMeta) *AtomImpl[T] {
1931
return &AtomImpl[T]{
2032
lock: &sync.Mutex{},
2133
val: initialVal,
2234
usedBy: make(map[string]bool),
35+
meta: meta,
2336
}
2437
}
2538

@@ -84,3 +97,13 @@ func (a *AtomImpl[T]) GetUsedBy() []string {
8497
}
8598
return keys
8699
}
100+
101+
func (a *AtomImpl[T]) GetMeta() *AtomMeta {
102+
a.lock.Lock()
103+
defer a.lock.Unlock()
104+
return a.meta
105+
}
106+
107+
func (a *AtomImpl[T]) GetAtomType() reflect.Type {
108+
return reflect.TypeOf((*T)(nil)).Elem()
109+
}

tsunami/engine/hooks.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ func UseLocal(vc *RenderContextImpl, initialVal any) string {
7777
atomName := "$local." + vc.GetCompWaveId() + "#" + strconv.Itoa(hookVal.Idx)
7878
if !hookVal.Init {
7979
hookVal.Init = true
80-
atom := MakeAtomImpl(initialVal)
80+
atom := MakeAtomImpl(initialVal, nil)
8181
vc.Root.RegisterAtom(atomName, atom)
8282
closedAtomName := atomName
8383
hookVal.UnmountFn = func() {

0 commit comments

Comments
 (0)