From 7ff326ef251311ed26b4fb43519c9374defdb47a Mon Sep 17 00:00:00 2001 From: Alex Dunmow Date: Mon, 1 Jun 2026 22:45:26 +0800 Subject: [PATCH] feat(sdk): ModFile struct, ParseModFull, and tests Co-Authored-By: Claude Opus 4.6 --- go.mod | 1 + go.sum | 2 + plugin/mod.go | 45 ++++++++++++++++++++ plugin/mod_test.go | 101 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 149 insertions(+) create mode 100644 plugin/mod.go create mode 100644 plugin/mod_test.go diff --git a/go.mod b/go.mod index a1ab21b..3bca224 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.26 require ( connectrpc.com/connect v1.19.2 + github.com/BurntSushi/toml v1.6.0 github.com/a-h/templ v0.3.1001 github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.9.2 diff --git a/go.sum b/go.sum index 668f94d..0928c72 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo= connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/a-h/templ v0.3.1001 h1:yHDTgexACdJttyiyamcTHXr2QkIeVF1MukLy44EAhMY= github.com/a-h/templ v0.3.1001/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/plugin/mod.go b/plugin/mod.go new file mode 100644 index 0000000..5f82f3c --- /dev/null +++ b/plugin/mod.go @@ -0,0 +1,45 @@ +package plugin + +import ( + tomlpkg "github.com/BurntSushi/toml" +) + +type ModFile struct { + Plugin ModPlugin `toml:"plugin"` + Compatibility *ModCompat `toml:"compatibility"` + Requires []ModRequirement `toml:"requires"` +} + +type ModPlugin struct { + Name string `toml:"name"` + Scope string `toml:"scope"` + Version string `toml:"version"` +} + +type ModCompat struct { + BlockCore string `toml:"block_core"` +} + +type ModRequirement struct { + Name string `toml:"name"` + Version string `toml:"version"` +} + +func ParseModFull(b []byte) (*ModFile, error) { + var m ModFile + if err := tomlpkg.Unmarshal(b, &m); err != nil { + return nil, err + } + return &m, nil +} + +func (m *ModFile) Coords() string { + if m == nil { + return "" + } + scope := m.Plugin.Scope + if scope == "" { + return m.Plugin.Name + "@" + m.Plugin.Version + } + return "@" + scope + "/" + m.Plugin.Name + "@" + m.Plugin.Version +} diff --git a/plugin/mod_test.go b/plugin/mod_test.go new file mode 100644 index 0000000..8d645dd --- /dev/null +++ b/plugin/mod_test.go @@ -0,0 +1,101 @@ +package plugin + +import ( + "testing" +) + +func TestParseModFull_BasicFields(t *testing.T) { + src := []byte(` +[plugin] +name = "smartblock" +scope = "blockninja" +version = "1.4.2" +`) + m, err := ParseModFull(src) + if err != nil { + t.Fatalf("ParseModFull err: %v", err) + } + if m.Plugin.Name != "smartblock" { + t.Errorf("Name = %q, want smartblock", m.Plugin.Name) + } + if m.Plugin.Scope != "blockninja" { + t.Errorf("Scope = %q, want blockninja", m.Plugin.Scope) + } + if m.Plugin.Version != "1.4.2" { + t.Errorf("Version = %q, want 1.4.2", m.Plugin.Version) + } + if got := m.Coords(); got != "@blockninja/smartblock@1.4.2" { + t.Errorf("Coords() = %q", got) + } +} + +func TestParseModFull_BackCompatNoScope(t *testing.T) { + src := []byte(` +[plugin] +name = "legacy" +version = "0.1.0" +`) + m, err := ParseModFull(src) + if err != nil { + t.Fatalf("ParseModFull err: %v", err) + } + if m.Plugin.Scope != "" { + t.Errorf("Scope should be empty, got %q", m.Plugin.Scope) + } + if got := m.Coords(); got != "legacy@0.1.0" { + t.Errorf("Coords() = %q, want legacy@0.1.0", got) + } +} + +func TestParseModFull_InvalidTOML(t *testing.T) { + _, err := ParseModFull([]byte("not valid toml = =")) + if err == nil { + t.Fatal("expected parse error") + } +} + +func TestParseModFull_EmptyInput(t *testing.T) { + m, err := ParseModFull(nil) + if err != nil { + t.Fatalf("nil input err: %v", err) + } + if m.Plugin.Name != "" { + t.Errorf("Name should be empty") + } +} + +func TestParseModFull_RequiresAndCompat(t *testing.T) { + src := []byte(` +[plugin] +name = "symposium" +scope = "blockninja" +version = "0.2.0" + +[compatibility] +block_core = ">=1.5 <2.0" + +[[requires]] +name = "@blockninja/smartblock" +version = ">=1.0 <2.0" + +[[requires]] +name = "@blockninja/gotham" +version = ">=1.2" +`) + m, err := ParseModFull(src) + if err != nil { + t.Fatalf("ParseModFull err: %v", err) + } + if m.Compatibility == nil || m.Compatibility.BlockCore != ">=1.5 <2.0" { + t.Errorf("Compat = %+v", m.Compatibility) + } + if len(m.Requires) != 2 { + t.Fatalf("Requires len = %d, want 2", len(m.Requires)) + } + if m.Requires[0].Name != "@blockninja/smartblock" { + t.Errorf("Requires[0].Name = %q", m.Requires[0].Name) + } + if m.Requires[1].Version != ">=1.2" { + t.Errorf("Requires[1].Version = %q", m.Requires[1].Version) + } +}