Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
### v0.9.0

- adds Break(key,value,...) for temporary runtime inspection.
- add optional filename to Dump()

### v0.8.1

- Explore replaces object with same root label.
Expand Down
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ Note: if the list contains just one structural value then selecting it can be sk

## explore while debugging

### Break

The following instruction will start the explorer on a struct, opens a Browser and provides a `resume` button to stop the explorer and resume the Go-routine that started it.

structexplorer.Break("myStruct", myStruct)

### Dump

Currently, the standard Go debugger `delve` stops all goroutines while in a debugging session.
This means that if you have started the `structexplorer` service in your program, it will not respond to any HTTP requests during that session.

Expand All @@ -57,10 +65,14 @@ The explorer can also be asked to dump an HTML page with the current state of va
s := structexplorer.NewService()
s.Explore("yours", yourStruct)
s.ExplorePath("yours.field") // dotted path of fields starting with an explore label
s.Dump()
s.Dump()
// or s.Dump("yourfile.html")

Another method is to use a special test case which starts an explorer at the end of a test and then run it with a longer acceptable timeout.

## examples

See folder `examples` for simple programs demonstrating each feature.


© 2025. https://ernestmicklei.com. MIT License
20 changes: 20 additions & 0 deletions examples/break/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package main

import (
"log"

"github.com/emicklei/structexplorer"
)

// go run .
func main() {
greeting := map[string]any{}
hello := struct{ Field string }{Field: "hello"}
greeting["hi"] = hello

log.Println("before opening the explorer to see state")

structexplorer.Break("map", greeting)

log.Println("after opening the explorer to see state")
}
22 changes: 22 additions & 0 deletions examples/break/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package main

import (
"log"
"testing"

"github.com/emicklei/structexplorer"
)

func TestWithBreak(t *testing.T) {
target := struct{ Field string }{Field: "hello"}

log.Println("before opening the explorer to see state")

structexplorer.Break("debugging", target)

log.Println("after opening the explorer to see state")

target.Field = "world"

structexplorer.Break("debugging", target)
}
4 changes: 2 additions & 2 deletions explore_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func RowColumn(row, column int) ExploreOption {
func Column(column int) ExploreOption {
return ExploreOption{
placement: func(e *explorer) (newRow, newColumn int) {
return e.nextFreeRow(column) + 1, column
return e.nextFreeRow(column), column
},
}
}
Expand All @@ -29,7 +29,7 @@ func Column(column int) ExploreOption {
func Row(row int) ExploreOption {
return ExploreOption{
placement: func(e *explorer) (newRow, newColumn int) {
return row, e.nextFreeColumn(row) + 1
return row, e.nextFreeColumn(row)
},
}
}
15 changes: 12 additions & 3 deletions explorer.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,17 @@ type explorer struct {
}

func (e *explorer) nextFreeColumn(row int) int {
max := 0
cols, ok := e.accessMap[row]
if !ok {
return 0
}
max := -1
for col := range cols {
if col > max {
max = col
}
}
return max
return max + 1
}

func (e *explorer) nextFreeRow(column int) int {
Expand All @@ -54,7 +54,13 @@ func (e *explorer) nextFreeRow(column int) int {
return row
}
}
return 0
max := -1
for row := range e.accessMap {
if row > max {
max = row
}
}
return max + 1
}

func (e *explorer) rootKeys() (list []string) {
Expand Down Expand Up @@ -164,6 +170,9 @@ func (e *explorer) putObjectStartingAt(row, col int, access objectAccess, option
}

func (e *explorer) buildIndexData(b *indexDataBuilder) indexData {
// was it starting using Break?
b.data.IsBreaking = b.isBreaking

for row, each := range e.accessMap {
for col, access := range each {
info := b.build(row, col, access)
Expand Down
23 changes: 22 additions & 1 deletion explorer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,27 @@ import (
"time"
)

func TestExplorerFreeColumn(t *testing.T) {
x := newExplorerOnAll()
r := x.nextFreeRow(0)
if got, want := r, 0; got != want {
t.Errorf("got [%[1]v:%[1]T] want [%[2]v:%[2]T]", got, want)
}
c := x.nextFreeColumn(0)
if got, want := c, 0; got != want {
t.Errorf("got [%[1]v:%[1]T] want [%[2]v:%[2]T]", got, want)
}
x.putObjectStartingAt(1, 1, objectAccess{}, Row(0))
r = x.nextFreeRow(0)
if got, want := r, 1; got != want {
t.Errorf("got [%[1]v:%[1]T] want [%[2]v:%[2]T]", got, want)
}
c = x.nextFreeColumn(1)
if got, want := c, 2; got != want {
t.Errorf("got [%[1]v:%[1]T] want [%[2]v:%[2]T]", got, want)
}
}

func TestExplorer(t *testing.T) {
x := newExplorerOnAll("indexData", indexData{})
d := x.buildIndexData(newIndexDataBuilder())
Expand Down Expand Up @@ -57,7 +78,7 @@ func TestExplorerTable(t *testing.T) {
if o2.object != o3.object {
t.Fail()
}
if got, want := x.nextFreeColumn(1), 1; got != want {
if got, want := x.nextFreeColumn(1), 2; got != want {
t.Errorf("got [%v]:%T want [%v]:%T", got, got, want, want)
}
if !x.canRemoveObjectAt(1, 1) {
Expand Down
9 changes: 5 additions & 4 deletions index_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import (
)

type indexDataBuilder struct {
data indexData
seq int
notLive bool
selectID string // id of the added fieldList (select element)
data indexData
seq int
notLive bool
isBreaking bool // service is started with Break(...)
selectID string // id of the added fieldList (select element)
}

func newIndexDataBuilder() *indexDataBuilder {
Expand Down
7 changes: 4 additions & 3 deletions index_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ var styleCSS string

type (
indexData struct {
Rows []tableRow
Script template.JS
Style template.CSS
Rows []tableRow
Script template.JS
Style template.CSS
IsBreaking bool
}
tableRow struct {
Cells []fieldList
Expand Down
5 changes: 5 additions & 0 deletions index_tmpl.html
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@
<span id="theme-toggle" class="theme-toggle" title="Toggle Theme"
>🔄</span
>
{{- if .IsBreaking }}
<button class="btn" title="resume from a break" onclick="javascript:resume();">
Resume from Breakpoint
</button>
{{- end }}
</p>

<script>
Expand Down
21 changes: 21 additions & 0 deletions open.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package structexplorer

import (
"fmt"
"os/exec"
"runtime"
)

// Open calls the OS default program for uri
func open(uri string) error {
switch {
case "windows" == runtime.GOOS:
return exec.Command("rundll32", "url.dll,FileProtocolHandler", uri).Start()
case "darwin" == runtime.GOOS:
return exec.Command("open", uri).Start()
case "linux" == runtime.GOOS:
return exec.Command("xdg-open", uri).Start()
default:
return fmt.Errorf("unable to open uri:%v on:%v", uri, runtime.GOOS)
Copy link

Copilot AI Oct 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing space in error message format string.

Suggested change
return fmt.Errorf("unable to open uri:%v on:%v", uri, runtime.GOOS)
return fmt.Errorf("unable to open uri: %v on: %v", uri, runtime.GOOS)

Copilot uses AI. Check for mistakes.
}
}
14 changes: 12 additions & 2 deletions script.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,18 @@ function explore(row, column, selectNode, action) {
action: action,
selections: getSelectValues(selectNode)
}));
xhr.onload = function () { window.location.reload(); }
xhr.onload = function() { window.location.reload(); }
}

function resume() {
const xhr = new XMLHttpRequest();
xhr.open("POST", window.location.href);
xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8")
Copy link

Copilot AI Oct 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon at end of statement. This is inconsistent with the coding style used elsewhere in the file.

Suggested change
xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8")
xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8");

Copilot uses AI. Check for mistakes.
xhr.send(JSON.stringify({
action: "resume"
}));
}

// Return an array of the selected option values in the control.
// Select is an HTML select element.
function getSelectValues(select) {
Expand All @@ -34,4 +44,4 @@ function getSelectValues(select) {
result.push(opt.value || opt.text);
}
return result;
}
}
Loading